diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index de22292a..b6dc8ddb 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,30 +1,24 @@
name: build
-on: [push, pull_request]
+on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- -
- name: Checkout code
+ - name: Checkout code
uses: actions/checkout@v3
- -
- name: Install Go
+ - name: Install Go
uses: actions/setup-go@v4
with:
- go-version: '1.20.x'
- -
- name: Install node
+ go-version: '1.22.x'
+ - name: Install node
uses: actions/setup-node@v3
with:
- node-version: '18'
+ node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- -
- name: Install dependencies
+ - name: Install dependencies
run: make build-deps-ubuntu
- -
- name: Build all the things
+ - name: Build all the things
run: make build
- -
- name: Print build results and checksums
+ - name: Print build results and checksums
run: make cli-build-results
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index b61e3361..80155e5b 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -7,35 +7,28 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- -
- name: Checkout code
+ - name: Checkout code
uses: actions/checkout@v3
- -
- name: Install Go
+ - name: Install Go
uses: actions/setup-go@v4
with:
- go-version: '1.20.x'
- -
- name: Install node
+ go-version: '1.22.x'
+ - name: Install node
uses: actions/setup-node@v3
with:
- node-version: '18'
+ node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- -
- name: Docker login
+ - name: Docker login
uses: docker/login-action@v2
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- -
- name: Install dependencies
+ - name: Install dependencies
run: make build-deps-ubuntu
- -
- name: Build and publish
+ - name: Build and publish
run: make release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- -
- name: Print build results and checksums
+ - name: Print build results and checksums
run: make cli-build-results
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index f76862a9..b0f99ffd 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,39 +1,30 @@
name: test
-on: [push, pull_request]
+on: [ push, pull_request ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- -
- name: Checkout code
+ - name: Checkout code
uses: actions/checkout@v3
- -
- name: Install Go
+ - name: Install Go
uses: actions/setup-go@v4
with:
- go-version: '1.20.x'
- -
- name: Install node
+ go-version: '1.22.x'
+ - name: Install node
uses: actions/setup-node@v3
with:
- node-version: '18'
+ node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- -
- name: Install dependencies
+ - name: Install dependencies
run: make build-deps-ubuntu
- -
- name: Build docs (required for tests)
+ - name: Build docs (required for tests)
run: make docs
- -
- name: Build web app (required for tests)
+ - name: Build web app (required for tests)
run: make web
- -
- name: Run tests, formatting, vetting and linting
+ - name: Run tests, formatting, vetting and linting
run: make check
- -
- name: Run coverage
+ - name: Run coverage
run: make coverage
- -
- name: Upload coverage to codecov.io
+ - name: Upload coverage to codecov.io
run: make coverage-upload
diff --git a/.gitignore b/.gitignore
index b60c9b23..7cbb52ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ secrets/
node_modules/
.DS_Store
__pycache__
-web/dev-dist/
\ No newline at end of file
+web/dev-dist/
+venv/
diff --git a/.goreleaser.yml b/.goreleaser.yml
index d3e71df2..062cce1f 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -164,14 +164,14 @@ dockers:
- image_templates:
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
use: buildx
- dockerfile: Dockerfile
+ dockerfile: Dockerfile-arm
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
- image_templates:
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
use: buildx
- dockerfile: Dockerfile
+ dockerfile: Dockerfile-arm
goarch: arm
goarm: 7
build_flag_templates:
@@ -179,7 +179,7 @@ dockers:
- image_templates:
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
use: buildx
- dockerfile: Dockerfile
+ dockerfile: Dockerfile-arm
goarch: arm
goarm: 6
build_flag_templates:
diff --git a/Dockerfile b/Dockerfile
index 7c2052ef..154a98ed 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,6 +9,8 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
+RUN apk add --no-cache tzdata \
+ && adduser -D -u 1000 ntfy
COPY ntfy /usr/bin
EXPOSE 80/tcp
diff --git a/Dockerfile-arm b/Dockerfile-arm
new file mode 100644
index 00000000..05b56fb1
--- /dev/null
+++ b/Dockerfile-arm
@@ -0,0 +1,19 @@
+FROM alpine
+
+LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
+LABEL org.opencontainers.image.url="https://ntfy.sh/"
+LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
+LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
+LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
+LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
+LABEL org.opencontainers.image.title="ntfy"
+LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
+
+# Alpine does not support adding "tzdata" on ARM anymore, see
+# https://github.com/binwiederhier/ntfy/issues/894
+
+RUN adduser -D -u 1000 ntfy
+COPY ntfy /usr/bin
+
+EXPOSE 80/tcp
+ENTRYPOINT ["ntfy"]
diff --git a/Dockerfile-build b/Dockerfile-build
index 62a60bd8..f454284b 100644
--- a/Dockerfile-build
+++ b/Dockerfile-build
@@ -1,14 +1,20 @@
-FROM golang:1.20-bullseye as builder
+FROM golang:1.21-bullseye as builder
ARG VERSION=dev
ARG COMMIT=unknown
+ARG NODE_MAJOR=18
-RUN apt-get update
-RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
-RUN apt-get install -y \
- build-essential \
- nodejs \
- python3-pip
+RUN apt-get update && apt-get install -y \
+ build-essential ca-certificates curl gnupg \
+ && mkdir -p /etc/apt/keyrings \
+ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
+ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
+ && apt-get update \
+ && apt-get install -y \
+ python3-pip \
+ python3-venv \
+ nodejs \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app
ADD Makefile .
@@ -19,7 +25,7 @@ RUN make docs-deps
ADD ./mkdocs.yml .
ADD ./docs ./docs
RUN make docs-build
-
+
# web
ADD ./web/package.json ./web/package-lock.json ./web/
RUN make web-deps
@@ -47,6 +53,7 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
+RUN adduser -D -u 1000 ntfy
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
EXPOSE 80/tcp
diff --git a/Makefile b/Makefile
index 8cb75238..4355423e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,6 @@
MAKEFLAGS := --jobs=1
+PYTHON := python3
+PIP := pip3
VERSION := $(shell git describe --tag)
COMMIT := $(shell git rev-parse --short HEAD)
@@ -39,8 +41,8 @@ help:
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo " make web-lint - Run eslint on the web app"
- @echo " make web-format - Run prettier on the web app"
- @echo " make web-format-check - Run prettier on the web app, but don't change anything"
+ @echo " make web-fmt - Run prettier on the web app"
+ @echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@@ -95,6 +97,7 @@ docker-dev:
--build-arg COMMIT=$(COMMIT) \
./
+
# Ubuntu-specific
build-deps-ubuntu:
@@ -103,32 +106,27 @@ build-deps-ubuntu:
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
+ python3 \
+ python3-venv \
jq
which pip3 || sudo apt-get install -y python3-pip
+
# Documentation
docs: docs-deps docs-build
-docs-build: .PHONY
- @if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
- if which python3.8; then \
- echo "python3.8 $(shell which mkdocs) build"; \
- python3.8 $(shell which mkdocs) build; \
- else \
- echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
- exit 1; \
- fi; \
- else \
- echo "mkdocs build"; \
- mkdocs build; \
- fi
+docs-venv: .PHONY
+ $(PYTHON) -m venv ./venv
-docs-deps: .PHONY
- pip3 install -r requirements.txt
+docs-build: docs-venv
+ (. venv/bin/activate && $(PYTHON) -m mkdocs build)
+
+docs-deps: docs-venv
+ (. venv/bin/activate && $(PIP) install -r requirements.txt)
docs-deps-update: .PHONY
- pip3 install -r requirements.txt --upgrade
+ (. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)
# Web app
@@ -151,10 +149,10 @@ web-deps:
web-deps-update:
cd web && npm update
-web-format:
+web-fmt:
cd web && npm run format
-web-format-check:
+web-fmt-check:
cd web && npm run format:check
web-lint:
@@ -248,7 +246,7 @@ cli-build-results:
# Test/check targets
-check: test web-format-check fmt-check vet web-lint lint staticcheck
+check: test web-fmt-check fmt-check vet web-lint lint staticcheck
test: .PHONY
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
@@ -275,7 +273,7 @@ coverage-upload:
# Lint/formatting targets
-fmt:
+fmt: web-fmt
gofmt -s -w .
fmt-check:
diff --git a/README.md b/README.md
index 1081f4f2..f91fd1c2 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
-[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy)
+[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy/v2)
[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy)
@@ -18,7 +18,7 @@ notification service. With ntfy, you can **send notifications to your phone or d
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
so since ntfy is open source.
-You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android)
+You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
@@ -31,7 +31,10 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a
## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
-I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $3.33/month** (if you use promo code `MYTOPIC`, limited time only). You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy). I would be very humbled by your sponsorship. ❤️
+I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of
+ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
+You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
+I would be very humbled by your sponsorship. ❤️
## **[Documentation](https://ntfy.sh/docs/)**
@@ -41,7 +44,7 @@ I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)
-## Chat / forum
+## Chat/forum
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
works best for you:
@@ -50,13 +53,13 @@ works best for you:
* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_)
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
-## Announcements / beta testers
+## Announcements/beta testers
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
## Contributing
-I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
+I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
@@ -143,6 +146,28 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
@@ -160,7 +185,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
-Third party libraries and resources:
+Third-party libraries and resources:
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
diff --git a/client/client.go b/client/client.go
index 93cf7da5..c2260966 100644
--- a/client/client.go
+++ b/client/client.go
@@ -7,8 +7,8 @@ import (
"encoding/json"
"errors"
"fmt"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"regexp"
diff --git a/client/client_test.go b/client/client_test.go
index f0b15a3f..a6784ff8 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -3,9 +3,9 @@ package client_test
import (
"fmt"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/client"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/test"
+ "heckel.io/ntfy/v2/client"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/test"
"os"
"testing"
"time"
diff --git a/client/config_test.go b/client/config_test.go
index c85d3d49..5d9eeecc 100644
--- a/client/config_test.go
+++ b/client/config_test.go
@@ -2,7 +2,7 @@ package client_test
import (
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/client"
+ "heckel.io/ntfy/v2/client"
"os"
"path/filepath"
"testing"
diff --git a/client/options.go b/client/options.go
index 630f1554..027b7fb5 100644
--- a/client/options.go
+++ b/client/options.go
@@ -2,7 +2,7 @@ package client
import (
"fmt"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
"net/http"
"strings"
"time"
diff --git a/cmd/access.go b/cmd/access.go
index 87f01d11..c6be94b5 100644
--- a/cmd/access.go
+++ b/cmd/access.go
@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
)
func init() {
diff --git a/cmd/access_test.go b/cmd/access_test.go
index 359beb92..81c9f2b9 100644
--- a/cmd/access_test.go
+++ b/cmd/access_test.go
@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/server"
- "heckel.io/ntfy/test"
+ "heckel.io/ntfy/v2/server"
+ "heckel.io/ntfy/v2/test"
"testing"
)
diff --git a/cmd/app.go b/cmd/app.go
index edef5b47..d88a9d58 100644
--- a/cmd/app.go
+++ b/cmd/app.go
@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
- "heckel.io/ntfy/log"
+ "heckel.io/ntfy/v2/log"
"os"
"regexp"
)
diff --git a/cmd/app_test.go b/cmd/app_test.go
index ec27a67d..f7d752f0 100644
--- a/cmd/app_test.go
+++ b/cmd/app_test.go
@@ -4,8 +4,8 @@ import (
"bytes"
"encoding/json"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/client"
- "heckel.io/ntfy/log"
+ "heckel.io/ntfy/v2/client"
+ "heckel.io/ntfy/v2/log"
"os"
"strings"
"testing"
diff --git a/cmd/config_loader.go b/cmd/config_loader.go
index 9f0a5769..e6180bed 100644
--- a/cmd/config_loader.go
+++ b/cmd/config_loader.go
@@ -5,7 +5,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
"os"
)
diff --git a/cmd/publish.go b/cmd/publish.go
index 5ffe3adf..aaec35e9 100644
--- a/cmd/publish.go
+++ b/cmd/publish.go
@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/client"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/client"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"io"
"os"
"os/exec"
diff --git a/cmd/publish_test.go b/cmd/publish_test.go
index a254f47d..31d01cb5 100644
--- a/cmd/publish_test.go
+++ b/cmd/publish_test.go
@@ -3,8 +3,8 @@ package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/test"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/test"
+ "heckel.io/ntfy/v2/util"
"net/http"
"net/http/httptest"
"os"
diff --git a/cmd/serve.go b/cmd/serve.go
index 5864fa46..60010fe0 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"github.com/stripe/stripe-go/v74"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
"io/fs"
"math"
"net"
@@ -17,12 +17,12 @@ import (
"syscall"
"time"
- "heckel.io/ntfy/log"
+ "heckel.io/ntfy/v2/log"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
- "heckel.io/ntfy/server"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/server"
+ "heckel.io/ntfy/v2/util"
)
func init() {
diff --git a/cmd/serve_test.go b/cmd/serve_test.go
index 774166c3..748adbd8 100644
--- a/cmd/serve_test.go
+++ b/cmd/serve_test.go
@@ -12,15 +12,11 @@ import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/client"
- "heckel.io/ntfy/test"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/client"
+ "heckel.io/ntfy/v2/test"
+ "heckel.io/ntfy/v2/util"
)
-func init() {
- rand.Seed(time.Now().UnixMilli())
-}
-
func TestCLI_Serve_Unix_Curl(t *testing.T) {
sockFile := filepath.Join(t.TempDir(), "ntfy.sock")
configFile := newEmptyFile(t) // Avoid issues with existing server.yml file on system
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index 77a1b5f1..1a0a7a6f 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/client"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/client"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"os"
"os/exec"
"os/user"
diff --git a/cmd/tier.go b/cmd/tier.go
index f1c8ddcb..63b023f9 100644
--- a/cmd/tier.go
+++ b/cmd/tier.go
@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
)
func init() {
diff --git a/cmd/tier_test.go b/cmd/tier_test.go
index 1774aa27..145f273e 100644
--- a/cmd/tier_test.go
+++ b/cmd/tier_test.go
@@ -3,8 +3,8 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/server"
- "heckel.io/ntfy/test"
+ "heckel.io/ntfy/v2/server"
+ "heckel.io/ntfy/v2/test"
"testing"
)
diff --git a/cmd/token.go b/cmd/token.go
index ab9f4447..cb92a130 100644
--- a/cmd/token.go
+++ b/cmd/token.go
@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"net/netip"
"time"
)
diff --git a/cmd/token_test.go b/cmd/token_test.go
index 40d7be7b..03295081 100644
--- a/cmd/token_test.go
+++ b/cmd/token_test.go
@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/server"
- "heckel.io/ntfy/test"
+ "heckel.io/ntfy/v2/server"
+ "heckel.io/ntfy/v2/test"
"regexp"
"testing"
)
diff --git a/cmd/user.go b/cmd/user.go
index a96c7089..af3afe54 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -6,13 +6,13 @@ import (
"crypto/subtle"
"errors"
"fmt"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
"os"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
)
const (
@@ -198,7 +198,6 @@ func execUserAdd(c *cli.Context) error {
if err != nil {
return err
}
-
password = p
}
if err := manager.AddUser(username, password, role); err != nil {
@@ -343,6 +342,8 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) {
password, err := util.ReadPassword(c.App.Reader)
if err != nil {
return "", err
+ } else if len(password) == 0 {
+ return "", errors.New("password cannot be empty")
}
fmt.Fprintf(c.App.ErrWriter, "\r%s\rconfirm: ", strings.Repeat(" ", 25))
confirm, err := util.ReadPassword(c.App.Reader)
diff --git a/cmd/user_test.go b/cmd/user_test.go
index 1149285f..e1bdd3ab 100644
--- a/cmd/user_test.go
+++ b/cmd/user_test.go
@@ -3,9 +3,9 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/server"
- "heckel.io/ntfy/test"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/server"
+ "heckel.io/ntfy/v2/test"
+ "heckel.io/ntfy/v2/user"
"os"
"path/filepath"
"testing"
diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go
index 1b364701..51926ca1 100644
--- a/cmd/webpush_test.go
+++ b/cmd/webpush_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "heckel.io/ntfy/server"
+ "heckel.io/ntfy/v2/server"
)
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
diff --git a/docs/config.md b/docs/config.md
index 23dbeb82..61d5f3a7 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -24,7 +24,7 @@ get a list of [command line options](#command-line-options).
The most basic settings are `base-url` (the external URL of the ntfy server), the HTTP/HTTPS listen address (`listen-http`
and `listen-https`), and socket path (`listen-unix`). All the other things are additional features.
-Here are a few working sample configs:
+Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
=== "server.yml (HTTP-only, with cache + attachments)"
``` yaml
@@ -44,6 +44,14 @@ Here are a few working sample configs:
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
+=== "server.yml (behind proxy, with cache + attachments)"
+ ``` yaml
+ base-url: "http://ntfy.example.com"
+ listen-http: ":2586"
+ cache-file: "/var/cache/ntfy/cache.db"
+ attachment-cache-dir: "/var/cache/ntfy/attachments"
+ ```
+
=== "server.yml (ntfy.sh config)"
``` yaml
# All the things: Behind a proxy, Firebase, cache, attachments,
@@ -65,6 +73,58 @@ Here are a few working sample configs:
keepalive-interval: "45s"
```
+Alternatively, you can also use command line arguments or environment variables to configure the server. Here's an example
+using Docker Compose (i.e. `docker-compose.yml`):
+
+=== "Docker Compose (w/ auth, cache, attachments)"
+ ``` yaml
+ version: '3'
+ services:
+ ntfy:
+ image: binwiederhier/ntfy
+ restart: unless-stopped
+ environment:
+ NTFY_BASE_URL: http://ntfy.example.com
+ NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
+ NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
+ NTFY_AUTH_DEFAULT_ACCESS: deny-all
+ NTFY_BEHIND_PROXY: true
+ NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
+ NTFY_ENABLE_LOGIN: true
+ volumes:
+ - ./:/var/lib/ntfy
+ ports:
+ - 80:80
+ command: serve
+ ```
+
+=== "Docker Compose (w/ auth, cache, web push, iOS)"
+ ``` yaml
+ version: '3'
+ services:
+ ntfy:
+ image: binwiederhier/ntfy
+ restart: unless-stopped
+ environment:
+ NTFY_BASE_URL: http://ntfy.example.com
+ NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
+ NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
+ NTFY_AUTH_DEFAULT_ACCESS: deny-all
+ NTFY_BEHIND_PROXY: true
+ NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
+ NTFY_ENABLE_LOGIN: true
+ NTFY_UPSTREAM_BASE_URL: https://ntfy.sh
+ NTFY_WEB_PUSH_PUBLIC_KEY:
+ NTFY_WEB_PUSH_PRIVATE_KEY:
+ NTFY_WEB_PUSH_FILE: /var/lib/ntfy/webpush.db
+ NTFY_WEB_PUSH_EMAIL_ADDRESS:
+ volumes:
+ - ./:/var/lib/ntfy
+ ports:
+ - 8093:80
+ command: serve
+ ```
+
## Message cache
If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
of time is important to allow [phones](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
@@ -344,10 +404,10 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
```
### Example: UnifiedPush
-[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
-has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.
+[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/developers/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
+has anonymous write access to the [topic](https://unifiedpush.org/developers/spec/definitions/#endpoint) used for push messages.
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the
-**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
+**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users-acl)** for more details.
To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either
allow anonymous write access for the entire prefix or explicitly per topic:
@@ -458,6 +518,31 @@ $ dig A mx1.ntfy.sh +short
3.139.215.220
```
+### Local-only email
+If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
+worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
+anything).
+
+=== "/etc/ntfy/server.yml"
+ ``` yaml
+ smtp-server-listen: ":25"
+ smtp-server-domain: "example.com"
+ smtp-server-addr-prefix: "ntfy-" # optional
+ ```
+
+Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
+ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
+fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
+In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
+`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
+
+So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
+topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
+to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
+
+If the internal service lets you use define an email "Subject", it will become the title of the notification.
+The body of the email will become the message of the notification.
+
## Behind a proxy (TLS, etc.)
!!! warning
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
@@ -649,8 +734,8 @@ or the root domain:
ServerName ntfy.sh
- # Proxy connections to ntfy (requires "a2enmod proxy")
- ProxyPass / http://127.0.0.1:2586/
+ # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
+ ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -658,19 +743,13 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
-
- # Enable mod_rewrite (requires "a2enmod rewrite")
- RewriteEngine on
-
- # WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
- RewriteCond %{HTTP:Upgrade} websocket [NC]
- RewriteCond %{HTTP:Connection} upgrade [NC]
- RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
- # it to work with curl without the annoying https:// prefix
- RewriteCond %{REQUEST_METHOD} GET
- RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
+ # it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
+
+ RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
+
+
@@ -681,8 +760,8 @@ or the root domain:
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
- # Proxy connections to ntfy (requires "a2enmod proxy")
- ProxyPass / http://127.0.0.1:2586/
+ # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
+ ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -690,14 +769,7 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
-
- # Enable mod_rewrite (requires "a2enmod rewrite")
- RewriteEngine on
-
- # WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
- RewriteCond %{HTTP:Upgrade} websocket [NC]
- RewriteCond %{HTTP:Connection} upgrade [NC]
- RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
+
```
@@ -1006,20 +1078,23 @@ By default, ntfy puts almost all rate limits on the message publisher, e.g. numb
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
of a topic's subscriber, instead of the limits of the publisher.**
-If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
-to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
-publishers (e.g. Matrix/Mastodon servers) are allowed to send.
+If subscriber-based rate limiting is enabled, **messages published on UnifiedPush topics** (topics starting with `up`, e.g. `up123456789012`)
+will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
-Once enabled, a client may send a `Rate-Topics: ,,...` header when subscribing to topics via
-HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
-to use when publishing on this topic. Note that setting the rate visitor requires **read-write permission** on the topic.
+Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
+a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
+requires **read-write permission** on the topic.
-UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
+If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
`visitor-message-daily-limit`.
To enable subscriber-based rate limiting, set `visitor-subscriber-rate-limiting: true`.
+!!! info
+ Due to a denial-of-service issue, support for the `Rate-Topics` header was removed entirely. This is unfortunate,
+ but subscriber-based rate limiting will still work for `up*` topics.
+
## Tuning for scale
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
@@ -1160,10 +1235,10 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
## Health checks
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
-If a non-200 HTTP status code is returned or if the returned `health` field is `false` the ntfy service should be considered as unhealthy.
+If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy.
```json
-{"health":true}
+{"healthy":true}
```
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
diff --git a/docs/deprecations.md b/docs/deprecations.md
index 99cdeeb9..56b1db85 100644
--- a/docs/deprecations.md
+++ b/docs/deprecations.md
@@ -1,4 +1,4 @@
-# Deprecation notices
+# Deprecations and breaking changes
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
diff --git a/docs/develop.md b/docs/develop.md
index 05b55773..e343503b 100644
--- a/docs/develop.md
+++ b/docs/develop.md
@@ -363,7 +363,7 @@ To build your own version with Firebase, you must:
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
* Then run:
```
-# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
+# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
@@ -429,7 +429,7 @@ steps:
### XCode setup
-1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
+1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
1. Similarly, install the SQLite.swift package dependency in XCode
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
diff --git a/docs/emojis.md b/docs/emojis.md
index fa01bb47..d801ae09 100644
--- a/docs/emojis.md
+++ b/docs/emojis.md
@@ -2,9 +2,9 @@
-You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
+You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
-[tagging and emojis page](../publish/#tags-emojis).
+[tagging and emojis page](publish.md#tags-emojis).
diff --git a/docs/examples.md b/docs/examples.md
index 8164e2bf..5396e318 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -135,6 +135,21 @@ You can send a message during a workflow run with curl. Here is an example sendi
${{ secrets.NTFY_URL }}
```
+## Changedetection.io
+ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
+[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
+uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
+
+To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
+to the notification list.
+
+For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
+
+In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
+the special ntfy Apprise Notification URL to the Notification List.
+
+![ntfy alerts on website change](static/img/cdio-setup.jpg)
+
## Watchtower (shoutrrr)
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
@@ -147,14 +162,23 @@ services:
image: containrrr/watchtower
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
+ - WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
```
+The environment variable `WATCHTOWER_NOTIFICATION_SKIP_TITLE` is required to prevent Watchtower from [replacing the `title` query parameter](https://containrrr.dev/watchtower/notifications/#settings). If omitted, the provided notification title will not be used.
+
Or, if you only want to send notifications using shoutrrr:
```
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
+Authentication tokens are also supported via the generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
+
+```
+generic+https://DOMAIN/TOPIC?@authorization=Bearer+TOKEN`
+```
+
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
diff --git a/docs/faq.md b/docs/faq.md
index 8844566f..6ff97cfe 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -76,6 +76,18 @@ However, if you still want to disable it, you can do so with the `web-root: disa
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
+## If topic names are public, could I not just brute force them?
+If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
+choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
+the topic is as good as a good password.
+
+As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
+In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
+choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
+could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
+
+For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
+
## Where can I donate?
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
diff --git a/docs/hooks.py b/docs/hooks.py
index cdb31a52..4a6957d9 100644
--- a/docs/hooks.py
+++ b/docs/hooks.py
@@ -1,6 +1,7 @@
import os
import shutil
-def copy_fonts(config, **kwargs):
- site_dir = config['site_dir']
- shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
+
+def on_post_build(config, **kwargs):
+ site_dir = config["site_dir"]
+ shutil.copytree("docs/static/fonts", os.path.join(site_dir, "get"))
diff --git a/docs/index.md b/docs/index.md
index 27314f1a..462a0fee 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,9 +3,9 @@ ntfy lets you **send push notifications to your phone or desktop via scripts fro
or POST requests. I use it to notify myself when scripts fail, or long-running commands complete.
## Step 1: Get the app
-
-
-
+
+
+
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
diff --git a/docs/install.md b/docs/install.md
index c7febac1..2777a3a4 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -14,14 +14,15 @@ We support amd64, armv7 and arm64.
1. Install ntfy using one of the methods described below
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
-3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
+3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
for details).
-If you like video tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU).
-It's short and to the point. _I am not affiliated with Kris, I just liked the video._
+If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
+[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
+resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
## Linux binaries
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
@@ -29,37 +30,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.tar.gz
- tar zxvf ntfy_2.6.2_linux_amd64.tar.gz
- sudo cp -a ntfy_2.6.2_linux_amd64/ntfy /usr/local/bin/ntfy
- sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_amd64/{client,server}/*.yml /etc/ntfy
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_amd64.tar.gz
+ tar zxvf ntfy_2.8.0_linux_amd64.tar.gz
+ sudo cp -a ntfy_2.8.0_linux_amd64/ntfy /usr/local/bin/ntfy
+ sudo mkdir /etc/ntfy && sudo cp ntfy_2.8.0_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.tar.gz
- tar zxvf ntfy_2.6.2_linux_armv6.tar.gz
- sudo cp -a ntfy_2.6.2_linux_armv6/ntfy /usr/bin/ntfy
- sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_armv6/{client,server}/*.yml /etc/ntfy
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv6.tar.gz
+ tar zxvf ntfy_2.8.0_linux_armv6.tar.gz
+ sudo cp -a ntfy_2.8.0_linux_armv6/ntfy /usr/bin/ntfy
+ sudo mkdir /etc/ntfy && sudo cp ntfy_2.8.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.tar.gz
- tar zxvf ntfy_2.6.2_linux_armv7.tar.gz
- sudo cp -a ntfy_2.6.2_linux_armv7/ntfy /usr/bin/ntfy
- sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_armv7/{client,server}/*.yml /etc/ntfy
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv7.tar.gz
+ tar zxvf ntfy_2.8.0_linux_armv7.tar.gz
+ sudo cp -a ntfy_2.8.0_linux_armv7/ntfy /usr/bin/ntfy
+ sudo mkdir /etc/ntfy && sudo cp ntfy_2.8.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.tar.gz
- tar zxvf ntfy_2.6.2_linux_arm64.tar.gz
- sudo cp -a ntfy_2.6.2_linux_arm64/ntfy /usr/bin/ntfy
- sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_arm64/{client,server}/*.yml /etc/ntfy
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_arm64.tar.gz
+ tar zxvf ntfy_2.8.0_linux_arm64.tar.gz
+ sudo cp -a ntfy_2.8.0_linux_arm64/ntfy /usr/bin/ntfy
+ sudo mkdir /etc/ntfy && sudo cp ntfy_2.8.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -109,7 +110,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -117,7 +118,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -125,7 +126,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -133,7 +134,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -143,28 +144,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -194,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
-To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_darwin_all.tar.gz),
+To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
-curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_darwin_all.tar.gz > ntfy_2.6.2_darwin_all.tar.gz
-tar zxvf ntfy_2.6.2_darwin_all.tar.gz
-sudo cp -a ntfy_2.6.2_darwin_all/ntfy /usr/local/bin/ntfy
+curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_darwin_all.tar.gz > ntfy_2.8.0_darwin_all.tar.gz
+tar zxvf ntfy_2.8.0_darwin_all.tar.gz
+sudo cp -a ntfy_2.8.0_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
-cp ntfy_2.6.2_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
+cp ntfy_2.8.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -223,7 +224,7 @@ brew install ntfy
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
-To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_windows_amd64.zip),
+To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
diff --git a/docs/integrations.md b/docs/integrations.md
index 57ae2c10..2dc4487d 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -23,6 +23,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
+- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
+- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
## Integration via HTTP/SMTP/etc.
@@ -56,7 +58,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
-- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
+- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
+- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
## CLIs + GUIs
@@ -80,7 +83,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
-- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
@@ -127,10 +129,35 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
+- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
+- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
+- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
+- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
+- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
+- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
+- [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go)
+- [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal)
+- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust)
## Blog + forum posts
-- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 9/2021
+- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
+- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
+- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
+- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
+- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
+- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
+- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
+- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
+- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
+- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
+- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
+- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
+- [How to Send Alerts From Raspberry Pi Pico W to a Phone or Tablet](https://www.tomshardware.com/how-to/send-alerts-raspberry-pi-pico-w-to-mobile-device) - tomshardware.com - 8/2023
+- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
+- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
+- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
+- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
@@ -214,6 +241,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
+| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
and uptime of third party servers, so use of each server is **at your own discretion**.
diff --git a/docs/known-issues.md b/docs/known-issues.md
index 401d82a1..cdb95bb6 100644
--- a/docs/known-issues.md
+++ b/docs/known-issues.md
@@ -27,11 +27,12 @@ Be sure that in your selfhosted server:
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
-## Firefox on Android not automatically subscribing to web push (see [#789](https://github.com/binwiederhier/ntfy/issues/789))
-ntfy defaults to web-push based subscriptions when installed as a [progressive web app](./subscribe/pwa.md). Firefox
-Android has an [open bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1796434) where it reports the PWA mode incorrectly.
-This causes ntfy to not automatically subscribe to web push, and requires you to go to the ntfy Settings page to enable
-it manually.
+## iOS app seeing "New message", but not real message content
+If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
+your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
+
+Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
+and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
## Safari does not play sounds for web push notifications
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
diff --git a/docs/publish.md b/docs/publish.md
index 2de0ff73..41370778 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -457,6 +457,7 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
=== "PowerShell"
``` powershell
$Request = @{
+ Method = 'POST'
URI = "https://ntfy.sh/phil_alerts"
Headers = @{
Priority = "5"
@@ -1033,7 +1034,7 @@ is the only required one:
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
- Body = @{
+ Body = ConvertTo-JSON @{
Topic = "mytopic"
Title = "Low disk space alert"
Message = "Disk space is low at 5.1 GB"
@@ -1042,7 +1043,7 @@ is the only required one:
FileName = "diskspace.jpg"
Tags = @("warning", "cd")
Click = "https://homecamera.lan/xasds1h2xsSsa/"
- Actions = ConvertTo-JSON @(
+ Actions = @(
@{
Action = "view"
Label = "Admin panel"
@@ -1130,7 +1131,7 @@ As of today, the following actions are supported:
when the action button is tapped (only supported on Android)
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
-Here's an example of what that a notification with actions can look like:
+Here's an example of what a notification with actions can look like:
![notification with actions](static/img/android-screenshot-notification-actions.png){ width=500 }
@@ -1919,10 +1920,10 @@ And the same example using [JSON publishing](#publish-as-json):
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
- Body = @{
+ Body = ConvertTo-Json -Depth 3 @{
Topic = "wifey"
Message = "Your wife requested you send a picture of yourself."
- Actions = ConvertTo-Json -Depth 3 @(
+ Actions = @(
@{
Action = "broadcast"
Label = "Take picture"
@@ -2072,7 +2073,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
'method' => 'POST',
'header' =>
"Content-Type: text/plain\r\n" .
- "Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
+ 'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
'content' => 'Garage door has been open for 15 minutes. Close it?'
]
]));
@@ -2199,10 +2200,10 @@ And the same example using [JSON publishing](#publish-as-json):
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
- Body = @{
+ Body = ConvertTo-Json -Depth 3 @{
Topic = "myhome"
Message = "Garage door has been open for 15 minutes. Close it?"
- Actions = ConvertTo-Json -Depth 3 @(
+ Actions = @(
@{
Action = "http"
Label = "Close door"
@@ -2287,7 +2288,7 @@ You can define which URL to open when a notification is clicked. This may be use
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
the web browser (or the app) and open the website.
-To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`).
+To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its alias `Click`).
If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
by another app, the responsible app may open.
diff --git a/docs/releases.md b/docs/releases.md
index 4cdbb312..3549330b 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -2,6 +2,68 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
+## ntfy iOS app v1.3
+Released Nov 26, 2023
+
+This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs for a long time, and I hope that they are finally fixed.
+
+Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
+
+**Bug fixes:**
+
+* UI not updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)/[#402](https://github.com/binwiederhier/ntfy/issues/402), thanks to [@tcaputi](https://github.com/tcaputi))
+
+### ntfy server v2.8.0
+Released November 19, 2023
+
+This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the `Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally), web app crash fixes
+
+**Bug fixes + maintenance:**
+
+* Support for HTML-only emails ([#690](https://github.com/binwiederhier/ntfy/issues/690)/[#693](https://github.com/binwiederhier/ntfy/pull/693), thanks to [@teastrainer](https://github.com/teastrainer) and [@CrazyWolf13](https://github.com/CrazyWolf13) for reporting)
+* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
+* Fix ACL issue with order of read/write rules ([#914](https://github.com/binwiederhier/ntfy/issues/914)/[#917](https://github.com/binwiederhier/ntfy/pull/917), thanks to [@sandman7920](https://github.com/sandman7920))
+* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
+* Add special logic to ignore `Priority` header if it resembles an RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
+* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
+* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
+
+**Additional languages:**
+
+* Finnish (thanks to [@Seppo](https://hosted.weblate.org/user/Seppo/))
+
+## ntfy server v2.7.0
+Released August 17, 2023
+
+This release ships Markdown support for the web app (not in the Android app yet), and adds support for
+right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
+internationalization support, a CLI auth bug.
+
+Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
+in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
+single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
+erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
+for details. **Please upgrade your ntfy instance if you run a multi-user system.**
+
+**Features:**
+
+* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
+* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
+
+**Security:** ⚠️
+
+* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
+
+**Bug fixes + maintenance:**
+
+* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
+* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
+* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
+
+**Documentation:**
+
+* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
+
## ntfy server v2.6.2
Released June 30, 2023
@@ -78,7 +140,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
## ntfy server v2.4.0
Released Apr 26, 2023
-This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`,
+This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
@@ -1241,7 +1303,7 @@ Released Dec 28, 2021
**Features & bug fixes:**
-* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
+* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
* Fixing the Santa bug #65
@@ -1251,18 +1313,18 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
-### ntfy server v2.7.0 (UNRELEASED)
-
-**Features:**
-
-* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
-* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
+### ntfy server v2.9.0
**Bug fixes + maintenance:**
-* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
-* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
-* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
+* Remove `Rate-Topics` header due to DoS security issue if `visitor-subscriber-rate-limiting: true` ([#1048](https://github.com/binwiederhier/ntfy/issues/1048))
+* Add non-root user to Docker image, ntfy can be run as non-root ([#967](https://github.com/binwiederhier/ntfy/pull/967)/[#966](https://github.com/binwiederhier/ntfy/issues/966), thanks to [@arahja](https://github.com/arahja))
+
+**Documentation:**
+
+* Remove `mkdocs-simple-hooks` ([#1016](https://github.com/binwiederhier/ntfy/pull/1016), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht))
+* Update Watchtower example ([#1014](https://github.com/binwiederhier/ntfy/pull/1014), thanks to [@lennart-m](https://github.com/lennart-m))
+* Fix dead links ([#1022](https://github.com/binwiederhier/ntfy/pull/1022), thanks to [@DerRockWolf](https://github.com/DerRockWolf))
### ntfy Android app v1.16.1 (UNRELEASED)
diff --git a/docs/static/img/cdio-setup.jpg b/docs/static/img/cdio-setup.jpg
new file mode 100644
index 00000000..2f9e44cb
Binary files /dev/null and b/docs/static/img/cdio-setup.jpg differ
diff --git a/docs/static/img/pwa-install-macos-safari-add-to-dock.png b/docs/static/img/pwa-install-macos-safari-add-to-dock.png
new file mode 100644
index 00000000..8a780605
Binary files /dev/null and b/docs/static/img/pwa-install-macos-safari-add-to-dock.png differ
diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md
index 58da9752..3f1c0e81 100644
--- a/docs/subscribe/api.md
+++ b/docs/subscribe/api.md
@@ -190,9 +190,10 @@ format. Keepalive messages are sent as empty lines.
## WebSockets
You may also subscribe to topics via [WebSockets](https://en.wikipedia.org/wiki/WebSocket), which is also widely
-supported in many languages. Most notably, WebSockets are natively supported in JavaScript. On the command line,
-I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat` or `curl`, but specifically
-for WebSockets.
+supported in many languages. Most notably, WebSockets are natively supported in JavaScript. You may also want to
+check out the [full example on GitHub](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-websocket).
+On the command line, I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat`
+or `curl`, but specifically for WebSockets.
The WebSockets endpoint is available at `/ws` and returns messages as JSON objects similar to the
[JSON stream endpoint](#subscribe-as-json-stream).
diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md
index 59cfc8e7..7f589d3c 100644
--- a/docs/subscribe/cli.md
+++ b/docs/subscribe/cli.md
@@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c
## Install + configure
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
-by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You
+by creating `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user). You
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
diff --git a/docs/subscribe/pwa.md b/docs/subscribe/pwa.md
index 582cb5ae..5dcaa257 100644
--- a/docs/subscribe/pwa.md
+++ b/docs/subscribe/pwa.md
@@ -26,6 +26,13 @@ app drawer:
+### Safari on macOS
+To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.
+
+
+
+
+
### Chrome/Firefox on Android
For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
diff --git a/examples/web-example-websocket/example-ws.html b/examples/web-example-websocket/example-ws.html
new file mode 100644
index 00000000..7025aa60
--- /dev/null
+++ b/examples/web-example-websocket/example-ws.html
@@ -0,0 +1,56 @@
+
+
+
+
+ ntfy.sh: WebSocket Example
+
+
+
+
+
ntfy.sh: WebSocket Example
+
+ This is an example showing how to use ntfy.sh with
+ WebSocket.
+ This example doesn't need a server. You can just save the HTML page and run it from anywhere.
+
" > "$1"
diff --git a/server/actions.go b/server/actions.go
index 80065873..98b90558 100644
--- a/server/actions.go
+++ b/server/actions.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
"regexp"
"strings"
"unicode/utf8"
diff --git a/server/config.go b/server/config.go
index 9815aa88..a0cfdcd5 100644
--- a/server/config.go
+++ b/server/config.go
@@ -5,7 +5,7 @@ import (
"net/netip"
"time"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
)
// Defines default config settings (excluding limits, see below)
diff --git a/server/config_test.go b/server/config_test.go
index 14f028f1..0dae5725 100644
--- a/server/config_test.go
+++ b/server/config_test.go
@@ -2,7 +2,7 @@ package server_test
import (
"github.com/stretchr/testify/assert"
- "heckel.io/ntfy/server"
+ "heckel.io/ntfy/v2/server"
"testing"
)
diff --git a/server/errors.go b/server/errors.go
index 27ba3df0..072bdc01 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -3,7 +3,7 @@ package server
import (
"encoding/json"
"fmt"
- "heckel.io/ntfy/log"
+ "heckel.io/ntfy/v2/log"
"net/http"
)
diff --git a/server/file_cache.go b/server/file_cache.go
index c097aefb..758d38ee 100644
--- a/server/file_cache.go
+++ b/server/file_cache.go
@@ -3,8 +3,8 @@ package server
import (
"errors"
"fmt"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"io"
"os"
"path/filepath"
diff --git a/server/file_cache_test.go b/server/file_cache_test.go
index 8f267a73..e7dee3b3 100644
--- a/server/file_cache_test.go
+++ b/server/file_cache_test.go
@@ -4,7 +4,7 @@ import (
"bytes"
"fmt"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
"os"
"strings"
"testing"
diff --git a/server/log.go b/server/log.go
index 978d0593..3d11ac47 100644
--- a/server/log.go
+++ b/server/log.go
@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"net/http"
"strings"
"unicode/utf8"
diff --git a/server/message_cache.go b/server/message_cache.go
index 8a613ff1..f0744abb 100644
--- a/server/message_cache.go
+++ b/server/message_cache.go
@@ -10,8 +10,8 @@ import (
"time"
_ "github.com/mattn/go-sqlite3" // SQLite driver
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
)
var (
diff --git a/server/server.go b/server/server.go
index 0ab36524..aad452ed 100644
--- a/server/server.go
+++ b/server/server.go
@@ -30,9 +30,9 @@ import (
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
)
// Server is the main server, providing the UI and API for ntfy
@@ -743,8 +743,8 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
return nil, e.With(t)
}
if unifiedpush && s.config.VisitorSubscriberRateLimiting && t.RateVisitor() == nil {
- // UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting (see
- // Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove
+ // UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting.
+ // The 5xx response is because some app servers (in particular Mastodon) will remove
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
@@ -1182,7 +1182,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
if err != nil {
return err
}
- poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
+ poll, since, scheduled, filters, err := parseSubscribeParams(r)
if err != nil {
return err
}
@@ -1212,7 +1212,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
}
return nil
}
- if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
+ if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
return err
}
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
@@ -1278,7 +1278,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
if err != nil {
return err
}
- poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
+ poll, since, scheduled, filters, err := parseSubscribeParams(r)
if err != nil {
return err
}
@@ -1364,7 +1364,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
}
return conn.WriteJSON(msg)
}
- if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
+ if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
return err
}
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
@@ -1397,7 +1397,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
return err
}
-func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, rateTopics []string, err error) {
+func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
poll = readBoolParam(r, false, "x-poll", "poll", "po")
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
since, err = parseSince(r, poll)
@@ -1408,7 +1408,6 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
if err != nil {
return
}
- rateTopics = readCommaSeparatedParam(r, "x-rate-topics", "rate-topics")
return
}
@@ -1420,9 +1419,8 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
// - or the topic is reserved, and v.user is the owner
// - or the topic is not reserved, and v.user has write access
//
-// Note: This TEMPORARILY also registers all topics starting with "up" (= UnifiedPush). This is to ease the transition
-// until the Android app will send the "Rate-Topics" header.
-func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic, rateTopics []string) error {
+// This only applies to UnifiedPush topics ("up...").
+func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic) error {
// Bail out if not enabled
if !s.config.VisitorSubscriberRateLimiting {
return nil
@@ -1431,7 +1429,7 @@ func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*top
// Make a list of topics that we'll actually set the RateVisitor on
eligibleRateTopics := make([]*topic, 0)
for _, t := range topics {
- if (strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength) || util.Contains(rateTopics, t.ID) {
+ if strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength {
eligibleRateTopics = append(eligibleRateTopics, t)
}
}
diff --git a/server/server.yml b/server/server.yml
index 3e92f742..b55b6844 100644
--- a/server/server.yml
+++ b/server/server.yml
@@ -277,15 +277,14 @@
# Rate limiting: Enable subscriber-based rate limiting (mostly used for UnifiedPush)
#
-# If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
-# to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
-# publishers (e.g. Matrix/Mastodon servers) are allowed to send.
+# If subscriber-based rate limiting is enabled, messages published on UnifiedPush topics** (topics starting with "up")
+# will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
#
-# Once enabled, a client may send a "Rate-Topics: ,,..." header when subscribing to topics via
-# HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
-# to use when publishing on this topic. Note: Setting the rate visitor requires READ-WRITE permission on the topic.
+# Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
+# a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
+# requires **read-write permission** on the topic.
#
-# UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
+# If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
# no "rate visitor" has been previously registered. This is to avoid burning the publisher's "visitor-message-daily-limit".
#
# visitor-subscriber-rate-limiting: false
@@ -342,6 +341,10 @@
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
#
+# Check your permissions:
+# If you are running ntfy with systemd, make sure this log file is owned by the
+# ntfy user and group by running: chown ntfy.ntfy .
+#
# Example (good for production):
# log-level: info
# log-format: json
diff --git a/server/server_account.go b/server/server_account.go
index f26cc2ff..cb841d07 100644
--- a/server/server_account.go
+++ b/server/server_account.go
@@ -2,9 +2,9 @@ package server
import (
"encoding/json"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"net/http"
"net/netip"
"strings"
diff --git a/server/server_account_test.go b/server/server_account_test.go
index 119efb16..4c269c2f 100644
--- a/server/server_account_test.go
+++ b/server/server_account_test.go
@@ -3,9 +3,9 @@ package server
import (
"fmt"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/netip"
"path/filepath"
diff --git a/server/server_admin.go b/server/server_admin.go
index 9380a5ff..fc9dfed1 100644
--- a/server/server_admin.go
+++ b/server/server_admin.go
@@ -1,7 +1,7 @@
package server
import (
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
"net/http"
)
diff --git a/server/server_admin_test.go b/server/server_admin_test.go
index 1513ea40..c2f8f95a 100644
--- a/server/server_admin_test.go
+++ b/server/server_admin_test.go
@@ -2,8 +2,8 @@ package server
import (
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"sync/atomic"
"testing"
"time"
diff --git a/server/server_firebase.go b/server/server_firebase.go
index b8158d2f..4a0cb7f9 100644
--- a/server/server_firebase.go
+++ b/server/server_firebase.go
@@ -8,8 +8,8 @@ import (
"firebase.google.com/go/v4/messaging"
"fmt"
"google.golang.org/api/option"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"strings"
)
diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go
index fb27ea05..9b653a29 100644
--- a/server/server_firebase_test.go
+++ b/server/server_firebase_test.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
"net/netip"
"strings"
"sync"
diff --git a/server/server_manager.go b/server/server_manager.go
index 66d449de..9f5fe888 100644
--- a/server/server_manager.go
+++ b/server/server_manager.go
@@ -1,8 +1,8 @@
package server
import (
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
"strings"
)
diff --git a/server/server_matrix.go b/server/server_matrix.go
index c25a1b59..f99bea8f 100644
--- a/server/server_matrix.go
+++ b/server/server_matrix.go
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"strings"
diff --git a/server/server_middleware.go b/server/server_middleware.go
index b1428154..b2ce6f70 100644
--- a/server/server_middleware.go
+++ b/server/server_middleware.go
@@ -3,7 +3,7 @@ package server
import (
"net/http"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/util"
)
type contextKey int
diff --git a/server/server_payments.go b/server/server_payments.go
index 1e98d059..334301bb 100644
--- a/server/server_payments.go
+++ b/server/server_payments.go
@@ -11,9 +11,9 @@ import (
"github.com/stripe/stripe-go/v74/price"
"github.com/stripe/stripe-go/v74/subscription"
"github.com/stripe/stripe-go/v74/webhook"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"net/netip"
diff --git a/server/server_payments_test.go b/server/server_payments_test.go
index ebd559e7..8da47a65 100644
--- a/server/server_payments_test.go
+++ b/server/server_payments_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/time/rate"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/netip"
"path/filepath"
diff --git a/server/server_test.go b/server/server_test.go
index 647268fb..8d965153 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -3,13 +3,13 @@ package server
import (
"bufio"
"context"
+ "crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"golang.org/x/crypto/bcrypt"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/user"
"io"
- "math/rand"
"net/http"
"net/http/httptest"
"net/netip"
@@ -24,8 +24,8 @@ import (
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
)
func TestMain(m *testing.M) {
@@ -329,6 +329,27 @@ func TestServer_PublishPriority(t *testing.T) {
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
}
+func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ response := request(t, s, "POST", "/mytopic", "test", map[string]string{
+ "Priority": "u=4",
+ "X-Priority": "5",
+ })
+ require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
+
+ response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{
+ "Priority": "u=9",
+ })
+ require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
+
+ response = request(t, s, "POST", "/mytopic", "test", map[string]string{
+ "p": "2",
+ "priority": "u=9, i",
+ })
+ require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
+}
+
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
@@ -491,6 +512,8 @@ func TestServer_PublishAtAndPrune(t *testing.T) {
messages := toMessages(t, response.Body.String())
require.Equal(t, 1, len(messages)) // Not affected by pruning
require.Equal(t, "a message", messages[0].Message)
+
+ time.Sleep(time.Second) // FIXME CI failing not sure why
}
func TestServer_PublishAndMultiPoll(t *testing.T) {
@@ -1323,9 +1346,7 @@ func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
// Register a UnifiedPush subscriber
- response := request(t, s, "GET", "/up123456789012/json?poll=1", "", map[string]string{
- "Rate-Topics": "up123456789012",
- })
+ response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil)
require.Equal(t, 200, response.Code)
// Publish message to topic
@@ -1356,9 +1377,7 @@ func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
// Register a UnifiedPush subscriber
- response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
- "Rate-Topics": "mytopic",
- })
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
require.Equal(t, 200, response.Code)
// Publish message to topic
@@ -1377,9 +1396,7 @@ func TestServer_PublishUnifiedPushText(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
// Register a UnifiedPush subscriber
- response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
- "Rate-Topics": "mytopic",
- })
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
require.Equal(t, 200, response.Code)
// Publish UnifiedPush text message
@@ -1411,9 +1428,7 @@ func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing.T) {
func TestServer_MatrixGateway_Push_Success(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
- response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
- "Rate-Topics": "mytopic", // Register first!
- })
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
require.Equal(t, 200, response.Code)
notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
@@ -2243,16 +2258,14 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
c.VisitorSubscriberRateLimiting = true
s := newTestServer(t, c)
- // "Register" visitor 1.2.3.4 to topic "subscriber1topic" as a rate limit visitor
+ // "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor
subscriber1Fn := func(r *http.Request) {
r.RemoteAddr = "1.2.3.4"
}
- rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
- "Rate-Topics": "subscriber1topic",
- }, subscriber1Fn)
+ rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn)
require.Equal(t, 200, rr.Code)
require.Equal(t, "", rr.Body.String())
- require.Equal(t, "1.2.3.4", s.topics["subscriber1topic"].rateVisitor.ip.String())
+ require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
// "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name)
subscriber2Fn := func(r *http.Request) {
@@ -2266,10 +2279,10 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
// Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the
// GET request before is also counted towards the request limiter.
for i := 0; i < 2; i++ {
- rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
+ rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
require.Equal(t, 200, rr.Code)
}
- rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil)
+ rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
require.Equal(t, 429, rr.Code)
// Publish another 2 messages to "up012345678912" as visitor 9.9.9.9
@@ -2302,14 +2315,12 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
// Subscriber rate limiting is disabled!
// Registering visitor 1.2.3.4 to topic has no effect
- rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
- "Rate-Topics": "subscriber1topic",
- }, func(r *http.Request) {
+ rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) {
r.RemoteAddr = "1.2.3.4"
})
require.Equal(t, 200, rr.Code)
require.Equal(t, "", rr.Body.String())
- require.Nil(t, s.topics["subscriber1topic"].rateVisitor)
+ require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
// Registering visitor 8.7.7.1 to topic has no effect
rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) {
@@ -2319,7 +2330,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
require.Equal(t, "", rr.Body.String())
require.Nil(t, s.topics["up012345678912"].rateVisitor)
- // Publish 3 messages to "subscriber1topic" as visitor 9.9.9.9
+ // Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9
for i := 0; i < 3; i++ {
rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
require.Equal(t, 200, rr.Code)
@@ -2392,80 +2403,30 @@ func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
subscriberFn := func(r *http.Request) {
r.RemoteAddr = "1.2.3.4"
}
- rr := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
- "rate-topics": "mytopic",
- }, subscriberFn)
+ rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn)
require.Equal(t, 200, rr.Code)
- require.Equal(t, "1.2.3.4", s.topics["mytopic"].rateVisitor.ip.String())
- require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
+ require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
+ require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
// Publish message, observe rate visitor tokens being decreased
- response := request(t, s, "POST", "/mytopic", "some message", nil)
+ response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
- require.Equal(t, int64(1), s.topics["mytopic"].rateVisitor.messagesLimiter.Value())
- require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
+ require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value())
+ require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
// Expire visitor
s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour)
s.pruneVisitors()
// Publish message again, observe that rateVisitor is not used anymore and is reset
- response = request(t, s, "POST", "/mytopic", "some message", nil)
+ response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
- require.Nil(t, s.topics["mytopic"].rateVisitor)
+ require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
require.Nil(t, s.visitors["ip:1.2.3.4"])
}
-func TestServer_SubscriberRateLimiting_ProtectedTopics(t *testing.T) {
- c := newTestConfigWithAuthFile(t)
- c.AuthDefault = user.PermissionDenyAll
- c.VisitorSubscriberRateLimiting = true
- s := newTestServer(t, c)
-
- // Create some ACLs
- require.Nil(t, s.userManager.AddTier(&user.Tier{
- Code: "test",
- MessageLimit: 5,
- }))
- require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
- require.Nil(t, s.userManager.ChangeTier("ben", "test"))
- require.Nil(t, s.userManager.AllowAccess("ben", "announcements", user.PermissionReadWrite))
- require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
- require.Nil(t, s.userManager.AllowAccess(user.Everyone, "public_topic", user.PermissionReadWrite))
-
- require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
- require.Nil(t, s.userManager.ChangeTier("phil", "test"))
- require.Nil(t, s.userManager.AddReservation("phil", "reserved-for-phil", user.PermissionReadWrite))
-
- // Set rate visitor as user "phil" on topic
- // - "reserved-for-phil": Allowed, because I am the owner
- // - "public_topic": Allowed, because it has read-write permissions for everyone
- // - "announcements": NOT allowed, because it has read-only permissions for everyone
- rr := request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
- "Authorization": util.BasicAuth("phil", "phil"),
- "Rate-Topics": "reserved-for-phil,public_topic,announcements",
- })
- require.Equal(t, 200, rr.Code)
- require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
- require.Equal(t, "phil", s.topics["public_topic"].rateVisitor.user.Name)
- require.Nil(t, s.topics["announcements"].rateVisitor)
-
- // Set rate visitor as user "ben" on topic
- // - "reserved-for-phil": NOT allowed, because I am not the owner
- // - "public_topic": Allowed, because it has read-write permissions for everyone
- // - "announcements": Allowed, because I have read-write permissions
- rr = request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
- "Authorization": util.BasicAuth("ben", "ben"),
- "Rate-Topics": "reserved-for-phil,public_topic,announcements",
- })
- require.Equal(t, 200, rr.Code)
- require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
- require.Equal(t, "ben", s.topics["public_topic"].rateVisitor.user.Name)
- require.Equal(t, "ben", s.topics["announcements"].rateVisitor.user.Name)
-}
-
func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.AuthDefault = user.PermissionReadWrite
diff --git a/server/server_twilio.go b/server/server_twilio.go
index 093abe63..9a8ef8ad 100644
--- a/server/server_twilio.go
+++ b/server/server_twilio.go
@@ -4,9 +4,9 @@ import (
"bytes"
"encoding/xml"
"fmt"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"net/url"
diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go
index af694a77..89a36051 100644
--- a/server/server_twilio_test.go
+++ b/server/server_twilio_test.go
@@ -2,8 +2,8 @@ package server
import (
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"net/http/httptest"
diff --git a/server/server_webpush.go b/server/server_webpush.go
index bb0f5408..cd41759d 100644
--- a/server/server_webpush.go
+++ b/server/server_webpush.go
@@ -8,8 +8,8 @@ import (
"strings"
"github.com/SherClockHolmes/webpush-go"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/user"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/user"
)
const (
diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go
index c0db79c6..c32c7bf8 100644
--- a/server/server_webpush_test.go
+++ b/server/server_webpush_test.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"github.com/stretchr/testify/require"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/user"
+ "heckel.io/ntfy/v2/util"
"io"
"net/http"
"net/http/httptest"
diff --git a/server/smtp_sender.go b/server/smtp_sender.go
index 9093687e..21eaf682 100644
--- a/server/smtp_sender.go
+++ b/server/smtp_sender.go
@@ -11,8 +11,8 @@ import (
"sync"
"time"
- "heckel.io/ntfy/log"
- "heckel.io/ntfy/util"
+ "heckel.io/ntfy/v2/log"
+ "heckel.io/ntfy/v2/util"
)
type mailer interface {
diff --git a/server/smtp_server.go b/server/smtp_server.go
index b9fbe6ee..467b8ca4 100644
--- a/server/smtp_server.go
+++ b/server/smtp_server.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/emersion/go-smtp"
+ "github.com/microcosm-cc/bluemonday"
"io"
"mime"
"mime/multipart"
@@ -14,6 +15,7 @@ import (
"net/http"
"net/http/httptest"
"net/mail"
+ "regexp"
"strings"
"sync"
)
@@ -27,6 +29,11 @@ var (
errUnsupportedContentType = errors.New("unsupported content type")
)
+var (
+ onlySpacesRegex = regexp.MustCompile(`(?m)^\s+$`)
+ consecutiveNewLinesRegex = regexp.MustCompile(`\n{3,}`)
+)
+
const (
maxMultipartDepth = 2
)
@@ -232,37 +239,66 @@ func readMailBody(body io.Reader, header mail.Header) (string, error) {
if err != nil {
return "", err
}
- if strings.ToLower(contentType) == "text/plain" {
- return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding"))
- } else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
- return readMultipartMailBody(body, params, 0)
+ canonicalContentType := strings.ToLower(contentType)
+ if canonicalContentType == "text/plain" || canonicalContentType == "text/html" {
+ return readTextMailBody(body, canonicalContentType, header.Get("Content-Transfer-Encoding"))
+ } else if strings.HasPrefix(canonicalContentType, "multipart/") {
+ return readMultipartMailBody(body, params)
}
return "", errUnsupportedContentType
}
-func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) {
+func readMultipartMailBody(body io.Reader, params map[string]string) (string, error) {
+ parts := make(map[string]string)
+ if err := readMultipartMailBodyParts(body, params, 0, parts); err != nil && err != io.EOF {
+ return "", err
+ } else if s, ok := parts["text/plain"]; ok {
+ return s, nil
+ } else if s, ok := parts["text/html"]; ok {
+ return s, nil
+ }
+ return "", io.EOF
+}
+
+func readMultipartMailBodyParts(body io.Reader, params map[string]string, depth int, parts map[string]string) error {
if depth >= maxMultipartDepth {
- return "", errMultipartNestedTooDeep
+ return errMultipartNestedTooDeep
}
mr := multipart.NewReader(body, params["boundary"])
for {
part, err := mr.NextPart()
if err != nil { // may be io.EOF
- return "", err
+ return err
}
partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
if err != nil {
- return "", err
+ return err
}
- if strings.ToLower(partContentType) == "text/plain" {
- return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
+ canonicalPartContentType := strings.ToLower(partContentType)
+ if canonicalPartContentType == "text/plain" || canonicalPartContentType == "text/html" {
+ s, err := readTextMailBody(part, canonicalPartContentType, part.Header.Get("Content-Transfer-Encoding"))
+ if err != nil {
+ return err
+ }
+ parts[canonicalPartContentType] = s
} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") {
- return readMultipartMailBody(part, partParams, depth+1)
+ if err := readMultipartMailBodyParts(part, partParams, depth+1, parts); err != nil {
+ return err
+ }
}
// Continue with next part
}
}
+func readTextMailBody(reader io.Reader, contentType, transferEncoding string) (string, error) {
+ if contentType == "text/plain" {
+ return readPlainTextMailBody(reader, transferEncoding)
+ } else if contentType == "text/html" {
+ return readHTMLMailBody(reader, transferEncoding)
+ }
+ return "", fmt.Errorf("unsupported content type: %s", contentType)
+}
+
func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) {
if strings.ToLower(transferEncoding) == "base64" {
reader = base64.NewDecoder(base64.StdEncoding, reader)
@@ -275,3 +311,21 @@ func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, e
}
return string(body), nil
}
+
+func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error) {
+ body, err := readPlainTextMailBody(reader, transferEncoding)
+ if err != nil {
+ return "", err
+ }
+ stripped := bluemonday.
+ StrictPolicy().
+ AddSpaceWhenStrippingTag(true).
+ Sanitize(body)
+ return removeExtraEmptyLines(stripped), nil
+}
+
+func removeExtraEmptyLines(s string) string {
+ s = onlySpacesRegex.ReplaceAllString(s, "")
+ s = consecutiveNewLinesRegex.ReplaceAllString(s, "\n\n")
+ return s
+}
diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go
index 7e1d29d9..90374ea8 100644
--- a/server/smtp_server_test.go
+++ b/server/smtp_server_test.go
@@ -568,6 +568,803 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep")
}
+func TestSmtpBackend_HTMLEmail(t *testing.T) {
+ email := `EHLO example.com
+MAIL FROM: test@mydomain.me
+RCPT TO: ntfy-mytopic@ntfy.sh
+DATA
+Message-Id: <51610934ss4.mmailer@fritz.box>
+From:
+To: ,
+
+Date: Thu, 30 Mar 2023 02:56:53 +0000
+Subject: A HTML email
+Mime-Version: 1.0
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<=21DOCTYPE html>
+
+
+Alerttitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+headertext of table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+" Very important information about a change in your
+home automation setup
+
+Now the light is on
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+If you don't want to receive this message anymore, stop the push
+ services in your FRITZ=21Box=2E
+Here you can see the active push services: "System > Push Service"=2E
+
+
+
+
+
+
+
+
+
+
+This mail has ben sent by your FRITZ=21Box automatically=2E
+
+
+
+
+
+
+
+
+
+
+
+.
+`
+
+ s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "/mytopic", r.URL.Path)
+ require.Equal(t, "A HTML email", r.Header.Get("Title"))
+ expected := `headertext of table
+
+" Very important information about a change in your
+home automation setup
+
+Now the light is on
+
+If you don't want to receive this message anymore, stop the push
+ services in your FRITZ!Box .
+Here you can see the active push services: "System > Push Service".
+
+This mail has ben sent by your FRITZ!Box automatically.`
+ require.Equal(t, expected, readAll(t, r.Body))
+ })
+ defer s.Close()
+ defer c.Close()
+ writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
+}
+
+const spamEmail = `
+EHLO example.com
+MAIL FROM: test@mydomain.me
+RCPT TO: ntfy-mytopic@ntfy.sh
+DATA
+Delivered-To: somebody@gmail.com
+Received: by 2002:a05:651c:1248:b0:2bf:c263:285 with SMTP id h8csp1096496ljh;
+ Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
+X-Google-Smtp-Source: AGHT+IFsB3WqbwbeefbeefbeefbeefbeefiXRNDHnIy2xBeaYHZCM3EC8DfPv55qDtgq9djTeBCF
+X-Received: by 2002:a05:6808:147:b0:3af:66e5:5d3c with SMTP id h7-20020a056808014700b003af66e55d3cmr11662458oie.26.1698672188132;
+ Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
+ARC-Seal: i=1; a=rsa-sha256; t=1698672188; cv=none;
+ d=google.com; s=arc-20160816;
+ b=XM96KvnTbr4h6bqrTPTuuDNXmFCr9Be/HvVhu+UsSQjP9RxPk0wDTPUPZ/HWIJs52y
+ beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef
+ BUmQ==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
+ h=list-unsubscribe-post:list-unsubscribe:mime-version:subject:to
+ :reply-to:from:date:message-id:dkim-signature:dkim-signature;
+ bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=;
+ fh=+kTCcNpX22TOI/SVSLygnrDqWeUt4zW7QKiv0TOVSGs=;
+ b=lyIBRuOxPOTY2s36OqP7M7awlBKd4t5PX9mJOEJB0eTnTZqML+cplrXUIg2ZTlAAi9
+ beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef
+ tgVQ==
+ARC-Authentication-Results: i=1; mx.google.com;
+ dkim=pass header.i=@spamspam.com header.s=2020294246 header.b=G8y6xmtK;
+ dkim=pass header.i=@auth.ccsend.com header.s=1000073432 header.b=ht8IksVK;
+ spf=pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) smtp.mailfrom="AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com";
+ dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=spamspam.com
+Return-Path:
+Received: from ccm30.constantcontact.com (ccm30.constantcontact.com. [208.75.123.226])
+ by mx.google.com with ESMTPS id h2-20020a05620a21c200b0076eeed38118si5450962qka.131.2023.10.30.06.23.07
+ for
+ (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
+ Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
+Received-SPF: pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) client-ip=208.75.123.226;
+Authentication-Results: mx.google.com;
+ dkim=pass header.i=@spamspam.com header.s=2020294246 header.b=G8y6xmtK;
+ dkim=pass header.i=@auth.ccsend.com header.s=1000073432 header.b=ht8IksVK;
+ spf=pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) smtp.mailfrom="AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com";
+ dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=spamspam.com
+Return-Path:
+Received: from [10.252.0.3] ([10.252.0.3:53254] helo=p2-jbemailsyndicator12.ctct.net) by 10.249.225.20 (envelope-from ) (ecelerity 4.3.1.999 r(:)) with ESMTP id A4/82-60517-B3EAF356; Mon, 30 Oct 2023 09:23:07 -0400
+DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256; c=relaxed/relaxed; s=2020294246; d=spamspam.com; h=date:mime-version:subject:X-Feedback-ID:X-250ok-CID:message-id:from:reply-to:list-unsubscribe:list-unsubscribe-post:to; bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=; b=G8y6xmtKv8asfEXA9o8dP+6foQjclo6j5sFREYVIJBbj5YJ5tqoiv5B04/qoRkoTBFDhmjt+BUua7AqDgPSnwbP2iPSA4fTJehnHhut1PyVUp/9vqSYlhxQehfdhma8tPg8ArKfYIKmfKJwKRaQBU0JHCaB1m+5LNQQX3UjkxAg=
+DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256; c=relaxed/relaxed; s=1000073432; d=auth.ccsend.com; h=date:mime-version:subject:X-Feedback-ID:X-250ok-CID:message-id:from:reply-to:list-unsubscribe:list-unsubscribe-post:to; bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=; b=ht8IksVKYY/Kb3dUERWoeW4eVdYjKL6F4PEoIZOhfFXor6XAIbPnd3A/CPmbmoqFZjnKh5OdcUy1N5qEoj8w1Q3TmN8/ySQkqrlrmSDSZIHZMY7Qp9/TJrqUe4RMFOO1KKIN6Y0vGP1+dWe98msMAHwvi2qMjG9aEKLfFr2JUTQ=
+Message-ID: <1140728754828.1133104752381.1941549819.0.260913JL.2002@synd.ccsend.com>
+Date: Mon, 30 Oct 2023 09:23:07 -0400 (EDT)
+From: spamspam Loan Servicing
+Reply-To: marklake@spamspam.com
+To: somebody@gmail.com
+Subject: Buying a home? You deserve the confidence of Pre-Approval
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="----=_Part_75055660_144854819.1698672187348"
+List-Unsubscribe:
+List-Unsubscribe-Post: List-Unsubscribe=One-Click
+X-Campaign-Activity-ID: 8a05de2a-5c88-44b1-be0e-f5a444cb0650
+X-250ok-CID: 8a05de2a-5c88-44b1-be0e-f5a444cb0650
+X-Channel-ID: b1441c50-a541-11ec-a79b-fa163e5bc304
+X-Return-Path-Hint: AbeefbeefbeefbeefbeefUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com
+X-Roving-Campaignid: 1140728754811
+X-Roving-Id: 1133104752381.1111111111
+X-Feedback-ID: b1441c50-a541-11ec-beef-beefbeefbeefbeef5de2a-5c88-44b1-be0e-f5a444cb0650:1133104752381:CTCT
+X-CTCT-ID: b13a9586-a541-11ec-beef-beefbeefbeef
+
+------=_Part_75055660_144854819.1698672187348
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+When you're buying a home, Pre-Approval gives you confidence you're in the =
+right price range and shows sellers you mean business. xxxxxxxxx SELLING or=
+ BUYING? Call: 844-590-2275 Get Your Homebuying PRE-APPROVAL IN 24-HOURS* G=
+et Pre-Approved When you're buying a home, Pre-Approval gives you confidenc=
+e you're in the right price range and shows sellers you mean business. xxx=
+xxxxxxGet Pre-Approved today! Click or Call to Get Pre-Approved 844-590-227=
+5 Get Pre-Approved nmlsconsumeraccess.org/ *The 24 hour timeframe is for mo=
+st approvals, however if additional information is needed or a request is o=
+n a holiday, the time for preapproval may be greater than 24 hours. This em=
+ail is for informational purposes only and is not an offer, loan approval o=
+r loan commitment. Mortgage rates are subject to change without notice. Som=
+e terms and restrictions may apply to certain loan programs. Refinancing ex=
+isting loans may result in total finance charges being higher over the life=
+ of the loan, reduction in payments may partially reflect a longer loan ter=
+m. This information is provided as guidance and illustrative purposes only =
+and does not constitute legal or financial advice. We are not liable or bou=
+nd legally for any answers provided to any user for our process or position=
+ on an issue. This information may change from time to time and at any time=
+ without notification. The most current information will be updated periodi=
+cally and posted in the online forum. spamspam Loan Servicing, LLC. NMLS#39=
+1521. nmlsconsumeraccess.org. You are receiving this information as a curre=
+nt loan customer with spamspam Loan Servicing, LLC. Not licensed for lendin=
+g activities in any of the U.S. territories. Not authorized to originate lo=
+ans in the State of New York. Licensed by the Dept. of Financial Protection=
+ and Innovation under the California Residential Mortgage .Lending Act #413=
+1216. This email was sent to somebody@gmail.com Version 103023PCHPrAp=
+9 xxxxxxxxx spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251, Coral =
+Gables, FL 33146-1837 Unsubscribe somebody@gmail.com Update Profile |=
+ Our Privacy Policy | Constant Contact Data Notice Sent by marklake@spamspa=
+m.com
+------=_Part_75055660_144854819.1698672187348
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+
+
+
When you're buying a home, Pre-Approval =
+gives you confidence you're in the right price range and shows sellers=
+ you mean business.
*The 24 hour timeframe is for=
+ most approvals, however if additional information is needed or a request i=
+s on a holiday, the time for preapproval may be greater than 24 hours.
+
This email is for informational purposes only and is not an offer,=
+ loan approval or loan commitment. Mortgage rates are subject to change wit=
+hout notice. Some terms and restrictions may apply to certain loan programs=
+. Refinancing existing loans may result in total finance charges being high=
+er over the life of the loan, reduction in payments may partially reflect a=
+ longer loan term. This information is provided as guidance and illustrativ=
+e purposes only and does not constitute legal or financial advice. We are n=
+ot liable or bound legally for any answers provided to any user for our pro=
+cess or position on an issue. This information may change from time to time=
+ and at any time without notification. The most current information will be=
+ updated periodically and posted in the online forum.
+
spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org.=
+ You are receiving this information as a current loan customer with spamspa=
+m Loan Servicing, LLC. Not licensed for lending activities in any of the U.=
+S. territories. Not authorized to originate loans in the State of New York.=
+ Licensed by the Dept. of Financial Protection and Innovation under the Cal=
+ifornia Residential Mortgage .Lending Act #4131216.
+
+
This email was sent to somebody@gmail.com
+
Version 103023PCHPrAp9
+
+
+
=
+
=
+
=
+
+
+
+
+
+
+
=
+
+
+------=_Part_75055660_144854819.1698672187348--
+.
+`
+
+func TestSmtpBackend_Spam_Text(t *testing.T) {
+ email := spamEmail
+ s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "/mytopic", r.URL.Path)
+ require.Equal(t, "Buying a home? You deserve the confidence of Pre-Approval", r.Header.Get("Title"))
+ actual := readAll(t, r.Body)
+ expected := "When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business. xxxxxxxxx SELLING or BUYING? Call: 844-590-2275 Get Your Homebuying PRE-APPROVAL IN 24-HOURS* Get Pre-Approved When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business. xxxxxxxxxGet Pre-Approved today! Click or Call to Get Pre-Approved 844-590-2275 Get Pre-Approved nmlsconsumeraccess.org/ *The 24 hour timeframe is for most approvals, however if additional information is needed or a request is on a holiday, the time for preapproval may be greater than 24 hours. This email is for informational purposes only and is not an offer, loan approval or loan commitment. Mortgage rates are subject to change without notice. Some terms and restrictions may apply to certain loan programs. Refinancing existing loans may result in total finance charges being higher over the life of the loan, reduction in payments may partially reflect a longer loan term. This information is provided as guidance and illustrative purposes only and does not constitute legal or financial advice. We are not liable or bound legally for any answers provided to any user for our process or position on an issue. This information may change from time to time and at any time without notification. The most current information will be updated periodically and posted in the online forum. spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org. You are receiving this information as a current loan customer with spamspam Loan Servicing, LLC. Not licensed for lending activities in any of the U.S. territories. Not authorized to originate loans in the State of New York. Licensed by the Dept. of Financial Protection and Innovation under the California Residential Mortgage .Lending Act #4131216. This email was sent to somebody@gmail.com Version 103023PCHPrAp9 xxxxxxxxx spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251, Coral Gables, FL 33146-1837 Unsubscribe somebody@gmail.com Update Profile | Our Privacy Policy | Constant Contact Data Notice Sent by marklake@spamspam.com"
+ require.Equal(t, expected, actual)
+ })
+ defer s.Close()
+ defer c.Close()
+ writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
+}
+
+func TestSmtpBackend_Spam_HTML(t *testing.T) {
+ email := strings.ReplaceAll(spamEmail, "text/plain", "text/not-plain-anymore") // We artificially force HTML parsing here
+ s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "/mytopic", r.URL.Path)
+ require.Equal(t, "Buying a home? You deserve the confidence of Pre-Approval", r.Header.Get("Title"))
+ actual := readAll(t, r.Body)
+ expected := `When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business.
+ ` + "\u200a" + `
+
+ SELLING or BUYING?
+ Call: 844-590-2275
+
+ Get Your Homebuying
+ PRE-APPROVAL IN 24-HOURS *
+ Get Pre-Approved
+
+ When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business.
+ ` + "\ufeff" + `Get Pre-Approved today!
+
+ Click or Call to Get Pre-Approved
+ 844-590-2275
+ Get Pre-Approved
+
+ nmlsconsumeraccess.org/
+ *The 24 hour timeframe is for most approvals, however if additional information is needed or a request is on a holiday, the time for preapproval may be greater than 24 hours.
+ This email is for informational purposes only and is not an offer, loan approval or loan commitment. Mortgage rates are subject to change without notice. Some terms and restrictions may apply to certain loan programs Refinancing existing loans may result in total finance charges being higher over the life of the loan, reduction in payments may partially reflect a longer loan term. This information is provided as guidance and illustrative purposes only and does not constitute legal or financial advice. We are not liable or bound legally for any answers provided to any user for our process or position on an issue. This information may change from time to time and at any time without notification. The most current information will be updated periodically and posted in the online forum.
+ spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org. You are receiving this information as a current loan customer with spamspam Loan Servicing, LLC. Not licensed for lending activities in any of the U.S. territories. Not authorized to originate loans in the State of New York. Licensed by the Dept. of Financial Protection and Innovation under the California Residential Mortgage .Lending Act #4131216.
+
+ This email was sent to somebody@gmail.com
+ Version 103023PCHPrAp9
+ ` + "\ufeff" + `
+
+ spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251 , Coral Gables, FL 33146-1837
+
+ Unsubscribe somebody@gmail.com
+
+ Update Profile |
+ Our Privacy Policy |
+ Constant Contact Data Notice
+
+Sent by
+ marklake@spamspam.com`
+ require.Equal(t, expected, actual)
+ })
+ defer s.Close()
+ defer c.Close()
+ writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
+}
+
+func TestSmtpBackend_HTMLOnly_FromDiskStation(t *testing.T) {
+ email := `EHLO example.com
+MAIL FROM: synology@mydomain.me
+RCPT TO: synology@mydomain.me
+DATA
+From: "=?UTF-8?B?Um9iYmll?="
+To:
+Message-Id: <640e6f562895d.6c9584bcfa491ac9c546b480b32ffc1d@mydomain.me>
+MIME-Version: 1.0
+Subject: =?UTF-8?B?W1N5bm9sb2d5IE5BU10gVGVzdCBNZXNzYWdlIGZyb20gTGl0dHNfTkFT?=
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+Congratulations! You have successfully set up the email notification on Synology_NAS. For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/. (If you cannot connect to the server, please contact the administrator.)
Selv om dette er uheldigt, giver det heller ikke ret meget mening at bruge ntfy-webappen i privat browsing-tilstand alligevel, fordi alt er gemt i browserens lager. Du kan læse mere om det i dette GitHub issue, eller tale med os på Discord eller Matrix.",
+ "publish_dialog_title_placeholder": "Notifikationstitel, f.eks. Advarsel om diskplads",
+ "account_basics_tier_description": "Din kontos niveau",
+ "account_basics_phone_numbers_description": "For notifikationer via telefonopkald",
+ "account_upgrade_dialog_cancel_warning": "Dette vil annullere dit abonnement og nedgradere din konto den {{date}}. På den dato slettes emnereservationer samt meddelelser, der er gemt på serveren.",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Ingen verificerede telefonnumre",
+ "publish_dialog_call_label": "Telefon opkald",
+ "account_usage_calls_title": "Telefonopkald foretaget",
+ "prefs_notifications_min_priority_description_any": "Viser alle notifikationer, uanset prioritet",
+ "error_boundary_gathering_info": "Indsaml mere info…",
+ "reservation_delete_dialog_action_keep_description": "Beskeder og vedhæftede filer, der er cachelagret på serveren, bliver offentligt synlige for personer med kendskab til emnenavnet.",
+ "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopieret til udklipsholder",
+ "prefs_reservations_dialog_description": "Reservering af et emne giver dig ejerskab over emnet og giver dig mulighed for at definere adgangstilladelser for andre brugere over emnet.",
+ "publish_dialog_title_topic": "Udgiv til {{topic}}",
+ "account_basics_phone_numbers_dialog_number_placeholder": "f.eks. +4512345678",
+ "account_basics_phone_numbers_dialog_code_placeholder": "f.eks. 123456",
+ "account_basics_username_description": "Hej, der er du ❤",
+ "publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com",
+ "account_basics_tier_interval_yearly": "årligt",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} årligt. Faktureres månedligt.",
+ "account_basics_phone_numbers_dialog_channel_call": "Opkald",
+ "publish_dialog_attachment_limits_file_and_quota_reached": "overskrider filgrænsen og kvoten på {{fileSizeLimit}}, {{remainingBytes}} tilbage",
+ "account_upgrade_dialog_interval_yearly": "årligt",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} faktureres årligt. Spar {{save}}.",
+ "account_usage_basis_ip_description": "Brugsstatistikker og begrænsninger for denne konto er baseret på din IP-adresse, så de kan være delt med andre brugere. Ovenstående grænser er omtrentlige baseret på de eksisterende hastigheds grænser.",
+ "account_basics_password_dialog_title": "Skift kodeord",
+ "account_basics_phone_numbers_title": "Telefonnumre",
+ "account_upgrade_dialog_interval_yearly_discount_save": "spar {{discount}}%",
+ "publish_dialog_drop_file_here": "Smid filen her",
+ "prefs_reservations_table_everyone_write_only": "Jeg kan udgive og abonnere, alle kan udgive",
+ "account_tokens_table_cannot_delete_or_edit": "Kan ikke redigere eller slette nuværende sessionstoken",
+ "publish_dialog_attached_file_filename_placeholder": "Vedhæftet filnavn",
+ "subscribe_dialog_subscribe_base_url_label": "Tjeneste-URL",
+ "account_upgrade_dialog_tier_price_per_month": "måned",
+ "message_bar_show_dialog": "Vis udgivelsesdialogen",
+ "account_usage_calls_none": "Der kan ikke foretages telefonopkald med denne konto",
+ "nav_upgrade_banner_description": "Reserver emner, flere beskeder og e-mails og større vedhæftede filer",
+ "publish_dialog_call_reset": "Fjern telefon opkald",
+ "account_basics_phone_numbers_dialog_code_label": "Verifikationskode",
+ "reservation_delete_dialog_action_delete_description": "Cachelagrede beskeder og vedhæftede filer slettes permanent. Denne handling kan ikke fortrydes.",
+ "alert_grant_button": "Tillad nu",
+ "account_usage_attachment_storage_description": "{{filesize}} pr. fil, slettet efter {{expiry}}",
+ "publish_dialog_chip_click_label": "Klik på URL",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Ring til mig",
+ "publish_dialog_call_item": "Ring til tlf. {{number}}",
+ "prefs_users_dialog_base_url_label": "Tjeneste-URL, f.eks. https://ntfy.sh",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_delete_dialog_billing_warning": "Hvis du sletter din konto, så annulleres dit abonnement med det samme. Du vil ikke længere have adgang til faktureringspanelet.",
+ "prefs_notifications_min_priority_description_max": "Vis notifikationer, hvis prioritet er 5 (maks.)",
+ "account_upgrade_dialog_reservations_warning_other": "Det valgte niveau tillader færre reserverede emner end dit nuværende niveau. Før du ændrer dit niveau, slet venligst mindst {{count}} reservationer. Du kan fjerne reservationer i Indstillinger."
}
diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json
index 7b5773ae..0f1d5b0a 100644
--- a/web/public/static/langs/de.json
+++ b/web/public/static/langs/de.json
@@ -25,7 +25,7 @@
"notifications_click_copy_url_title": "Link-URL in Zwischenablage kopieren",
"publish_dialog_priority_low": "Niedrige Priorität",
"publish_dialog_message_label": "Nachricht",
- "action_bar_unsubscribe": "Von Thema abmelden",
+ "action_bar_unsubscribe": "Abbestellen",
"notifications_copied_to_clipboard": "In Zwischenablage kopiert",
"notifications_loading": "Benachrichtigungen werden geladen …",
"notifications_attachment_open_title": "Gehe zu {{url}}",
@@ -82,7 +82,7 @@
"publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_placeholder": "Dateiname des Anhangs",
"publish_dialog_delay_label": "Verzögerung",
- "publish_dialog_email_placeholder": "E-Mail-Adresse, an welche die Benachrichtigung gesendet werden soll, z. B. phil@example.com",
+ "publish_dialog_email_placeholder": "E-Mail-Adresse, an welche die Benachrichtigung gesendet werden soll, z.B. phil@example.com",
"publish_dialog_chip_click_label": "Klick-URL",
"publish_dialog_button_cancel_sending": "Senden abbrechen",
"publish_dialog_drop_file_here": "Datei hierher ziehen",
@@ -154,7 +154,7 @@
"notifications_actions_not_supported": "Diese Aktion wird in der Web-App nicht unterstützt",
"notifications_actions_http_request_title": "Sende HTTP {{method}} an {{url}}",
"action_bar_show_menu": "Menü anzeigen",
- "action_bar_toggle_mute": "Stummschaltung der Benachrichtigungen an/aus",
+ "action_bar_toggle_mute": "Stummschaltung an/aus",
"message_bar_show_dialog": "Dialog zur Veröffentlichung anzeigen",
"message_bar_publish": "Benachrichtigung veröffentlichen",
"nav_button_connecting": "verbinde",
@@ -180,7 +180,7 @@
"error_boundary_unsupported_indexeddb_description": "Die ntfy Web-App benötigt eine IndexedDB für eine korrekte Funktion, und Dein Browser unterstützt in privaten Tabs keinen IndexedDB.
Das ist zwar ärgerlich, eine Nutzung von ntfy in einem privaten Tab macht aber auch wenig Sinn da alle Daten im Browser gespeichert werden. Weitere Informationen gibt es in diesem GitHub-Issue, oder im Chat bei Discord oder Matrix.",
"action_bar_toggle_action_menu": "Aktionsmenü öffnen/schließen",
"notifications_new_indicator": "Neue Benachrichtigung",
- "publish_dialog_email_reset": "Email-Weiterleitung entfernen",
+ "publish_dialog_email_reset": "E-Mail-Weiterleitung entfernen",
"action_bar_logo_alt": "ntfy Logo",
"nav_button_muted": "Benachrichtigungen stummgeschaltet",
"notifications_list_item": "Benachrichtigung",
@@ -217,7 +217,7 @@
"signup_form_password": "Kennwort",
"signup_form_toggle_password_visibility": "Kennwort-Sichtbarkeit umschalten",
"nav_button_account": "Konto",
- "nav_upgrade_banner_description": "Themen reservieren, mehr Nachrichten & Emails, größere Anhänge",
+ "nav_upgrade_banner_description": "Themen reservieren, mehr Nachrichten & E-Mails und größere Anhänge",
"display_name_dialog_title": "Anzeigennamen ändern",
"display_name_dialog_placeholder": "Anzeigename",
"reserve_dialog_checkbox_label": "Thema reservieren und Zugriffsrechte konfigurieren",
@@ -245,7 +245,7 @@
"account_basics_tier_payment_overdue": "Deine Zahlung ist überfällig. Bitte aktualisiere Deine Zahlungsmethode, oder Dein Konto wird herabgestuft.",
"account_basics_tier_manage_billing_button": "Zahlung verwalten",
"account_usage_messages_title": "Veröffentlichte Nachrichten",
- "account_usage_emails_title": "Gesendete Emails",
+ "account_usage_emails_title": "Gesendete E-Mails",
"account_usage_reservations_title": "Reservierte Themen",
"account_usage_reservations_none": "Keine reservierten Themen für dieses Konto",
"account_usage_attachment_storage_title": "Speicherplatz für Anhänge",
@@ -266,7 +266,7 @@
"account_upgrade_dialog_reservations_warning_other": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. Bitte löschen vor dem Wechsel Deines Levels mindestens {{count}} Reservierungen. Du kannst Reservierungen in den Einstellungen löschen.",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reservierte Themen",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} Nachrichten pro Tag",
- "account_upgrade_dialog_tier_features_emails_other": "{{emails}} Emails pro Tag",
+ "account_upgrade_dialog_tier_features_emails_other": "{{emails}} E-Mails pro Tag",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pro Datei",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} gesamter Speicherplatz",
"account_upgrade_dialog_tier_selected_label": "Ausgewählt",
@@ -310,7 +310,7 @@
"prefs_reservations_delete_button": "Zugriff auf Thema zurücksetzen",
"prefs_reservations_table": "Übersicht reservierter Themen",
"prefs_reservations_table_topic_header": "Thema",
- "prefs_reservations_table_everyone_deny_all": "Nur kann veröffentlichen und lesen",
+ "prefs_reservations_table_everyone_deny_all": "Nur ich kann veröffentlichen und lesen",
"prefs_reservations_table_everyone_write_only": "Ich kann veröffentlichen und lesen, jeder kann veröffentlichen",
"prefs_reservations_table_not_subscribed": "Nicht abonniert",
"prefs_reservations_table_click_to_subscribe": "Klicken um zu abonnieren",
diff --git a/web/public/static/langs/eo.json b/web/public/static/langs/eo.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/web/public/static/langs/eo.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json
new file mode 100644
index 00000000..11b815f0
--- /dev/null
+++ b/web/public/static/langs/fi.json
@@ -0,0 +1,384 @@
+{
+ "publish_dialog_message_placeholder": "Kirjoita viesti tähän",
+ "account_upgrade_dialog_tier_features_no_calls": "Ei puheluita",
+ "account_upgrade_dialog_billing_contact_email": "Laskutukseen liittyvissä kysymyksissä contact us suoraan.",
+ "account_tokens_dialog_title_create": "Luo käyttöoikeustunnus",
+ "prefs_reservations_dialog_title_edit": "Muokkaa varattua topikkia",
+ "account_basics_tier_interval_monthly": "Kuukausittain",
+ "publish_dialog_checkbox_publish_another": "Julkaise toinen",
+ "publish_dialog_details_examples_description": "Katso esimerkkejä ja yksityiskohtaisen kuvauksen kaikista lähetysominaisuuksista dokumentaatiosta.",
+ "account_basics_tier_canceled_subscription": "Tilauksesi peruutettiin ja se muutetaan maksuttomaksi tiliksi {{date}}.",
+ "priority_default": "oletus",
+ "prefs_notifications_min_priority_title": "Vähimmäisprioriteetti",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} päivittäisiä puheluja",
+ "account_upgrade_dialog_tier_current_label": "Nykyinen",
+ "action_bar_account": "Kirjautuminen",
+ "publish_dialog_filename_placeholder": "Liitetiedoston nimi",
+ "account_basics_password_dialog_current_password_incorrect": "Salasana virheellinen",
+ "account_tokens_table_token_header": "Token",
+ "prefs_notifications_delete_after_never": "Ei koskaan",
+ "prefs_users_description": "Lisää/poista käyttäjiä suojatuista topikeista täällä. Huomaa, että käyttäjätunnus ja salasana on tallennettu selaimen paikalliseen tallennustilaan.",
+ "account_basics_phone_numbers_dialog_number_label": "Puhelinnumero",
+ "subscribe_dialog_subscribe_description": "Aiheet eivät välttämättä ole salasanasuojattuja, joten valitse nimi, jota ei ole helposti arvatavissa. Kun olet tilannut, voit käyttää PUT/POST ilmoituksia.",
+ "action_bar_logo_alt": "ntfy logo",
+ "account_basics_password_dialog_button_submit": "Vaihda salasana",
+ "publish_dialog_emoji_picker_show": "Valitse emoji",
+ "account_basics_username_title": "Käyttäjätunnus",
+ "login_disabled": "Kirjautuminen poissa käytöstä",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Vahvista koodi",
+ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "säästä jopa {{discount}}%",
+ "account_tokens_dialog_label": "Etiketti, esim. Tutka-ilmoitukset",
+ "common_add": "Lisää",
+ "account_tokens_table_expires_header": "Vanhenee",
+ "account_upgrade_dialog_proration_info": "Osuus suhde: Kun päivität maksullisten pakettien välillä, hintaero veloitetaan välittömästi. Kun siirryt alemmalle tasolle, saldoa käytetään tulevien laskutuskausien maksamiseen.",
+ "prefs_reservations_dialog_access_label": "Oikeudet",
+ "account_usage_attachment_storage_title": "Liiteiden säilytys",
+ "prefs_users_dialog_username_label": "Username, esim pena",
+ "message_bar_error_publishing": "Virhe ilmoituksen julkaisemisessa",
+ "publish_dialog_chip_delay_label": "Viivästytä toimitusta",
+ "account_usage_messages_title": "Julkaistut viestit",
+ "notifications_attachment_open_button": "Avaa liite",
+ "emoji_picker_search_clear": "Tyhjennä haku",
+ "prefs_reservations_table_not_subscribed": "Ei tilattu",
+ "publish_dialog_topic_placeholder": "Topikin nimi, esim. erkin_hälyt",
+ "account_upgrade_dialog_tier_features_emails_other": "{{emails}} päivittäisiä emaileja",
+ "prefs_notifications_min_priority_max_only": "Vain maksimi prioriteetti",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} päivittäisiä puheluja",
+ "prefs_notifications_sound_description_some": "Ilmoitukset soittavat {{sound}} äänen saapuessaan",
+ "prefs_reservations_edit_button": "Muokkaa topikin oikeuksia",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Lähetä SMS",
+ "account_basics_tier_change_button": "Vaihda",
+ "account_tokens_dialog_expires_never": "Käyttöoikeus ei vanhene koskaan",
+ "subscribe_dialog_login_title": "Kirjautuminen vaaditaan",
+ "account_tokens_dialog_expires_x_days": "Tunnus vanhenee {{days}} päivän kuluttua",
+ "notifications_new_indicator": "Uusi ilmoitus",
+ "prefs_reservations_table_everyone_read_only": "Minä voin julkaista ja tilata, kaikki voivat tilata",
+ "prefs_reservations_table_everyone_deny_all": "Vain minä voin julkaista ja tilata",
+ "publish_dialog_chip_topic_label": "Vaihda topikkia",
+ "account_basics_phone_numbers_dialog_description": "Jotta voit käyttää puheluilmoitusominaisuutta, sinun on lisättävä ja vahvistettava vähintään yksi puhelinnumero. Vahvistus voidaan tehdä tekstiviestillä tai puhelimitse.",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} varatut topikit",
+ "publish_dialog_tags_placeholder": "Pilkuilla eroteltu luettelo tunnisteista, esim. varoitus, srv1-varmuuskopio",
+ "account_delete_title": "Poista tili",
+ "publish_dialog_attached_file_remove": "Poista liitetiedosto",
+ "nav_button_connecting": "yhdistetään",
+ "account_delete_dialog_label": "Salasana",
+ "subscribe_dialog_login_button_login": "Kirjaudu",
+ "account_upgrade_dialog_tier_features_no_reservations": "Ei varattuja topikkeja",
+ "message_bar_type_message": "Kirjoita viesti tähän",
+ "publish_dialog_base_url_label": "Palvelun URL",
+ "signup_form_confirm_password": "Vahvista salasana",
+ "prefs_users_table_cannot_delete_or_edit": "Kirjautunutta käyttäjää ei voi poistaa tai muokata",
+ "account_basics_tier_admin_suffix_with_tier": "(mukana {{tier}} tier)",
+ "prefs_notifications_delete_after_three_hours_description": "Ilmoitukset poistetaan automaattisesti kolmen tunnin kuluttua",
+ "publish_dialog_chip_email_label": "Lähetä sähköpostiin",
+ "publish_dialog_attach_label": "Liitteen URL-osoite",
+ "signup_form_username": "Käyttäjätunnus",
+ "prefs_notifications_delete_after_three_hours": "Kolmen tunnin jälkeen",
+ "nav_button_muted": "Ilmoitukset mykistetty",
+ "action_bar_profile_settings": "Asetukset",
+ "signup_error_creation_limit_reached": "Tilin lisäämisraja saavutettu",
+ "notifications_attachment_open_title": "Siirry osoitteeseen {{url}}",
+ "prefs_notifications_min_priority_description_x_or_higher": "Näytä ilmoitukset, jos prioriteetti on {{number}} ({{name}}) tai suurempi",
+ "reservation_delete_dialog_description": "Varauksen poistaminen luopuu topikin omistajuudesta ja antaa muiden varata sen. Voit säilyttää tai poistaa olemassa olevia viestejä ja liitteitä.",
+ "subscribe_dialog_login_username_label": "Käyttäjätunnus, esim. pentti",
+ "subscribe_dialog_error_user_not_authorized": "Käyttäjää {{username}} ei ole valtuutettu",
+ "prefs_reservations_table_everyone_read_write": "Jokainen voi julkaista ja tilata",
+ "prefs_reservations_dialog_title_delete": "Poista topikin varaus",
+ "prefs_users_table": "Käyttäjä taulukko",
+ "prefs_reservations_table_topic_header": "Topikki",
+ "action_bar_toggle_mute": "Hiljennä/poista hiljennys",
+ "reservation_delete_dialog_submit_button": "Poista varaus",
+ "account_basics_title": "Tili",
+ "nav_button_documentation": "Dokumentointi",
+ "prefs_reservations_limit_reached": "Olet saavuttanut varattujen topikkien rajan.",
+ "account_upgrade_dialog_interval_monthly": "Kuukausittain",
+ "prefs_users_add_button": "Lisää käyttäjä",
+ "account_upgrade_dialog_tier_features_messages_other": "{{messages}} päivittäisiä viestejä",
+ "publish_dialog_delay_reset": "Poista viivästetty toimitus",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Ei puhelinnumeroita vielä",
+ "action_bar_toggle_action_menu": "Avaa/sulje toiminto valikko",
+ "subscribe_dialog_subscribe_button_generate_topic_name": "Luo nimi",
+ "notifications_list_item": "Ilmoitus",
+ "prefs_appearance_language_title": "Kieli",
+ "notifications_attachment_link_expired": "latauslinkki vanhentunut",
+ "subscribe_dialog_login_password_label": "Salasana",
+ "prefs_notifications_delete_after_one_day_description": "Ilmoitukset poistetaan automaattisesti yhden päivän kuluttua",
+ "subscribe_dialog_subscribe_button_subscribe": "Tilaa",
+ "account_tokens_table_never_expires": "Ei vanhene koskaan",
+ "account_tokens_delete_dialog_title": "Poista käyttöoikeustunnus",
+ "prefs_notifications_delete_after_one_month": "Kuukauden kuluttua",
+ "publish_dialog_chip_call_label": "Puhelu",
+ "account_basics_phone_numbers_dialog_title": "Lisää puhelinnumero",
+ "account_tokens_delete_dialog_description": "Ennen kuin poistat käyttöoikeustunnuksen, varmista, että mikään sovellus tai komentosarja ei käytä sitä aktiivisesti. Tätä toimintoa ei voi kumota.",
+ "nav_button_all_notifications": "Kaikki ilmoitukset",
+ "account_upgrade_dialog_button_cancel": "Peruuta",
+ "notifications_attachment_image": "Liitekuva",
+ "account_tokens_table_label_header": "Merkki",
+ "notifications_attachment_file_document": "muu asiakirja",
+ "publish_dialog_button_cancel": "Peruuta",
+ "account_upgrade_dialog_billing_contact_website": "Laskutukseen liittyvissä kysymyksissä käy website.",
+ "signup_form_button_submit": "Kirjaudu linkki",
+ "account_basics_username_admin_tooltip": "Olet pääkäyttäjä",
+ "prefs_notifications_delete_after_never_description": "Ilmoituksia eivät koskaan poisteta automaattisesti",
+ "account_delete_dialog_description": "Tämä poistaa pysyvästi tilisi, mukaan lukien kaikki palvelimelle tallennetut tiedot. Poistamisen jälkeen käyttäjätunnuksesi on poissa käytöstä 7 päivään. Jos todella haluat jatkaa, vahvista salasanasi alla olevaan kenttään.",
+ "publish_dialog_email_reset": "Poista sähköpostin edelleenlähetys",
+ "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} varatut topikit",
+ "account_usage_reservations_none": "Tälle tilille ei ole varattu topikkeja",
+ "prefs_notifications_sound_description_none": "Ilmoitukset eivät toista ääntä saapuessaan",
+ "account_tokens_description": "Käytä käyttjätunnuksia, kun julkaiset ja tilaat ntfy API:n kautta, jotta sinun ei tarvitse lähettää tilisi tunnistetietoja. Katso lisätietoja documentation.",
+ "common_back": "Takaisin",
+ "prefs_reservations_table": "Varattujen topikkien taulukko",
+ "emoji_picker_search_placeholder": "Etsi emoji",
+ "subscribe_dialog_subscribe_topic_placeholder": "Topikin nimi, esim. pentin_hälyt",
+ "account_upgrade_dialog_button_cancel_subscription": "Peruuta tilaus",
+ "notifications_attachment_file_audio": "äänitiedosto",
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} päivittäisiä emaileja",
+ "action_bar_sign_up": "Kirjautuminen",
+ "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} tiedostokoko",
+ "notifications_mark_read": "Merkitse luetuksi",
+ "prefs_reservations_description": "Voit varata topikien nimiä henkilökohtaiseen käyttöön täältä. Aiheen varaaminen antaa sinulle topikin omistajuuden ja voit määrittää topikkiin liittyviä käyttöoikeuksia muille käyttäjille.",
+ "notifications_attachment_copy_url_title": "Kopioi liitteen URL-osoite leikepöydälle",
+ "account_usage_title": "Käytössä",
+ "account_basics_tier_upgrade_button": "Päivitä Pro versioon",
+ "prefs_users_description_no_sync": "Käyttäjiä ja salasanoja ei ole synkronoitu tiliisi.",
+ "account_tokens_dialog_title_edit": "Muokkaa käyttöoikeustunnusta",
+ "nav_button_publish_message": "Julkaise ilmoitus",
+ "prefs_users_table_base_url_header": "Palvelin URL",
+ "notifications_click_copy_url_title": "Kopioi linkin URL-osoite leikepöydälle",
+ "publish_dialog_attach_reset": "Poista liitteen URL-osoite",
+ "account_upgrade_dialog_tier_features_messages_one": "{{messages}} päivittäisiä viestejä",
+ "account_upgrade_dialog_reservations_warning_one": "Valittu taso sallii vähemmän varattuja topikeita kuin nykyinen tasosi. Ennen kuin muutat tasosi, poista vähintään yksi varaus. Voit poistaa varauksia Asetuksista.",
+ "common_copy_to_clipboard": "Kopioi leikkelepöydälle",
+ "alert_not_supported_description": "Selaimesi ei tue ilmoituksia.",
+ "subscribe_dialog_error_topic_already_reserved": "Topikki on jo varattu",
+ "message_bar_publish": "Julkaise viesti",
+ "alert_grant_description": "Myönnä selaimelle lupa näyttää työpöytäilmoituksia.",
+ "prefs_users_table_user_header": "Käyttäjä",
+ "error_boundary_stack_trace": "Pinon jälki",
+ "prefs_users_dialog_password_label": "Salasana",
+ "prefs_notifications_delete_after_one_week": "Viikon kuluttua",
+ "publish_dialog_priority_low": "Matala tärkeys",
+ "publish_dialog_priority_label": "Prioriteetti",
+ "prefs_reservations_delete_button": "Poista topikin oikeudet",
+ "account_basics_tier_admin_suffix_no_tier": "(e tasoa)",
+ "prefs_notifications_delete_after_one_week_description": "Ilmoitukset poistetaan automaattisesti viikon kuluttua",
+ "error_boundary_unsupported_indexeddb_description": "Ntfy-verkkosovellus tarvitsee IndexedDB:n toimiakseen, eikä selaimesi tue IndexedDB:tä yksityisessä selaustilassa.
Vaikka tämä on valitettavaa, ntfy-verkon käyttäminen ei myöskään ole kovin järkevää yksityisessä selaustilassa, koska kaikki on tallennettu selaimen tallennustilaan. Voit lukea siitä lisää tästä GitHub-numerosta tai puhua meille Discordissa tai Matrixissa.",
+ "subscribe_dialog_subscribe_button_cancel": "Peruuta",
+ "notifications_attachment_copy_url_button": "Kopioi URL",
+ "account_basics_tier_payment_overdue": "Maksusi on myöhässä. Päivitä maksutapasi, tai tilisi poistetaan pian.",
+ "publish_dialog_title_placeholder": "Ilmoituksen otsikko, esim. Levytilan hälytys",
+ "account_basics_tier_description": "Tilisi taso",
+ "account_basics_phone_numbers_description": "Puheluilmoituksia varten",
+ "prefs_reservations_dialog_title_add": "Varaa topikki",
+ "account_basics_tier_free": "Vapaa",
+ "account_upgrade_dialog_cancel_warning": "Tämä peruuttaa tilauksesi ja alentaa tilisi {{date}}. Tuona päivänä topikit sekä palvelimen välimuistissa olevat viestit poistetaan.",
+ "notifications_click_copy_url_button": "Kopioi linkki",
+ "account_basics_tier_admin": "Admin",
+ "subscribe_dialog_subscribe_title": "Tilaa topikki",
+ "nav_topics_title": "Tilatut aiheet",
+ "prefs_notifications_sound_title": "Ilmoitusääni",
+ "prefs_notifications_min_priority_default_and_higher": "Oletusprioriteetti ja korkeammat",
+ "prefs_reservations_table_access_header": "Oikeudet",
+ "action_bar_show_menu": "Näytä menu",
+ "action_bar_settings": "Asetukset",
+ "notifications_copied_to_clipboard": "Kopioitu leikepöydälle",
+ "account_delete_dialog_button_cancel": "Peruuta",
+ "publish_dialog_delay_placeholder": "Toimituksen viivästyminen, esim. {{unixTimestamp}}, {{relativeTime}} tai \"{{naturalLanguage}}\" (vain englanti)",
+ "account_tokens_table_copied_to_clipboard": "Käyttöoikeustunnus kopioitu",
+ "alert_grant_title": "Ilmoitukset on poistettu käytöstä",
+ "account_tokens_dialog_expires_x_hours": "Tunnus vanhenee {{hours}} tunnin kuluttua",
+ "prefs_users_edit_button": "Muokkaa käyttäjää",
+ "account_upgrade_dialog_title": "Muuta tilitasoa",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Ei vahvistettuja puhelinnumeroita",
+ "priority_low": "matala",
+ "prefs_reservations_table_click_to_subscribe": "Tilaa napsauttamalla",
+ "account_basics_password_description": "Vaihda tilisi salasana",
+ "publish_dialog_call_label": "Puhelu",
+ "account_usage_calls_title": "Soitetut puhelut",
+ "error_boundary_description": "Näin ei selvästikään pitäisi tapahtua. Pahoittelut tästä. Jos sinulla on hetki aikaa, ilmoita tästä GitHubissa tai ilmoita meille Discordin tai Matrix kautta.",
+ "signup_form_toggle_password_visibility": "Vaihda salasanan näkyvyys",
+ "login_link_signup": "Kirjaudu linkki",
+ "publish_dialog_message_label": "Viesti",
+ "publish_dialog_attached_file_title": "Liitetiedosto:",
+ "priority_min": "min",
+ "action_bar_sign_in": "Kirjaudu sisään",
+ "action_bar_unsubscribe": "Peruuta tilaus",
+ "account_basics_tier_basic": "Perus",
+ "signup_title": "Lisää ntfy tili",
+ "prefs_notifications_min_priority_description_any": "Näytetään kaikki ilmoitukset tärkeydestä riippumatta",
+ "error_boundary_gathering_info": "Kerää lisätietoja…",
+ "publish_dialog_priority_max": "Max. prioriteetti",
+ "error_boundary_unsupported_indexeddb_title": "Yksityistä selaamista ei tueta",
+ "prefs_notifications_delete_after_one_day": "Yhden päivän jälkeen",
+ "error_boundary_title": "Voi ei, ntfy kaatui",
+ "action_bar_change_display_name": "Näyttönimen vaihtaminen",
+ "notifications_attachment_file_app": "Android-sovellustiedosto",
+ "alert_not_supported_context_description": "Ilmoituksia tuetaan vain HTTPS:n kautta. Tämä on Ilmoitussovellusliittymän rajoitus.",
+ "reservation_delete_dialog_action_keep_description": "Palvelimelle välimuistiin tallennetut viestit ja liitteet tulevat julkiseksi topikin nimen tietävälle henkilölle.",
+ "prefs_reservations_add_button": "Lisää varattu topik",
+ "prefs_reservations_title": "Varatut topikit",
+ "account_basics_phone_numbers_copied_to_clipboard": "Puhelinnumero kopioitu leikepöydälle",
+ "prefs_reservations_dialog_description": "Topikin varaaminen antaa sinulle aiheen omistajuuden ja voit määrittää aiheeseen liittyviä käyttöoikeuksia muille käyttäjille.",
+ "account_basics_tier_title": "Tilin tyyppi",
+ "account_usage_cannot_create_portal_session": "Laskutusportaalin avaaminen epäonnistui",
+ "account_tokens_delete_dialog_submit_button": "Poista tunnus pysyvästi",
+ "account_delete_description": "Poista tilisi pysyvästi",
+ "account_basics_phone_numbers_dialog_number_placeholder": "esim. +35812345678",
+ "account_basics_phone_numbers_dialog_code_placeholder": "esim. 123456",
+ "prefs_notifications_title": "Ilmoitukset",
+ "account_basics_tier_manage_billing_button": "Hallinnoi laskutusta",
+ "account_tokens_title": "Käyttöoikeudet",
+ "publish_dialog_email_label": "Email",
+ "account_basics_username_description": "Hei, se olet sinä ❤",
+ "prefs_reservations_dialog_topic_label": "Topik",
+ "account_basics_password_dialog_confirm_password_label": "Vahvista salasana",
+ "action_bar_reservation_edit": "Muokkaa varausta",
+ "publish_dialog_base_url_placeholder": "Palvelun URL-osoite, esim. https://example.com",
+ "prefs_users_title": "Hallinnoi käyttäjiä",
+ "account_basics_tier_interval_yearly": "vuosittain",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} Laskutetaan kuukausittain.",
+ "action_bar_clear_notifications": "Poista kaikki ilmoitukset",
+ "account_delete_dialog_button_submit": "Poista tili pysyvästi",
+ "account_basics_phone_numbers_dialog_channel_call": "Soitto",
+ "account_basics_password_title": "Salasana",
+ "account_basics_password_dialog_new_password_label": "Uusi salasana",
+ "nav_upgrade_banner_label": "Päivitä ntfy Prohon",
+ "account_tokens_dialog_expires_unchanged": "Jätä viimeinen käyttöpäivä ennalleen",
+ "publish_dialog_delay_label": "Viive",
+ "error_boundary_button_copy_stack_trace": "Kopioi pinon jälki",
+ "publish_dialog_button_send": "Lähetä",
+ "action_bar_reservation_delete": "Poista varaus",
+ "publish_dialog_button_cancel_sending": "Peruuta lähetys",
+ "account_tokens_dialog_title_delete": "Poista käyttöoikeustunnus",
+ "account_usage_of_limit": "limiitistä {{limit}}",
+ "publish_dialog_attach_placeholder": "Liitä tiedosto URL-osoitteen mukaan, esim. https://f-droid.org/F-Droid.apk",
+ "publish_dialog_email_placeholder": "Osoite, johon ilmoitus välitetään, esim. urpo@example.com",
+ "notifications_attachment_link_expires": "linkki vanhenee {{date}}",
+ "action_bar_send_test_notification": "Lähetä testi ilmoitus",
+ "reservation_delete_dialog_action_keep_title": "Säilytä välimuistissa olevat viestit ja liitteet",
+ "prefs_notifications_sound_no_sound": "Ei ääntä",
+ "account_upgrade_dialog_interval_yearly": "Vuosittain",
+ "publish_dialog_tags_label": "Tagit",
+ "signup_form_password": "Salasana",
+ "action_bar_reservation_limit_reached": "Raja saavutettu",
+ "account_upgrade_dialog_button_redirect_signup": "Kirjaudu nyt",
+ "publish_dialog_click_placeholder": "URL-osoite, joka avautuu, kun ilmoitusta napsautetaan",
+ "alert_not_supported_title": "Ilmoituksia ei tueta",
+ "account_tokens_dialog_button_cancel": "Peruuta",
+ "subscribe_dialog_error_user_anonymous": "Anonyymi",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Tallenna {{save}}.",
+ "prefs_notifications_min_priority_high_and_higher": "Korkea prioriteetti ja korkeammat",
+ "account_usage_basis_ip_description": "Tämän tilin käyttötilastot ja rajoitukset perustuvat IP-osoitteeseesi, joten ne voidaan jakaa muiden käyttäjien kanssa. Yllä esitetyt rajat ovat likimääräisiä perustuen olemassa oleviin rajoituksiin.",
+ "publish_dialog_priority_high": "Korkea prioriteetti",
+ "login_form_button_submit": "Kirjaudu",
+ "account_basics_password_dialog_title": "Vaihda salasana",
+ "priority_max": "max",
+ "notifications_attachment_file_image": "kuvatiedosto",
+ "account_usage_limits_reset_daily": "Käyttörajat nollataan päivittäin keskiyöllä (UTC)",
+ "account_usage_unlimited": "Rajoittamaton",
+ "prefs_users_delete_button": "Poista käyttäjä",
+ "publish_dialog_click_label": "Napsauta URL-osoitetta",
+ "prefs_notifications_min_priority_any": "Kaikki prioriteetit",
+ "account_tokens_dialog_expires_label": "Käyttöoikeustunnus vanhenee",
+ "publish_dialog_filename_label": "Tiedostonimi",
+ "publish_dialog_chip_attach_file_label": "Liitä paikallinen tiedosto",
+ "account_basics_phone_numbers_title": "Puhelinnumerot",
+ "prefs_notifications_delete_after_title": "Poista ilmoitukset",
+ "account_upgrade_dialog_interval_yearly_discount_save": "säästä {{discount}}%",
+ "signup_disabled": "Kirjautuminen estetty",
+ "publish_dialog_drop_file_here": "Pudota tiedosto tähän",
+ "prefs_users_dialog_title_edit": "Muokkaa käyttäjää",
+ "account_basics_password_dialog_current_password_label": "Nykyinen salasana",
+ "prefs_notifications_min_priority_low_and_higher": "Matala prioriteetti ja korkeammat",
+ "action_bar_profile_title": "Profiili",
+ "account_tokens_dialog_button_update": "Päivitä tunnus",
+ "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} lopullinen tiedostokoko",
+ "publish_dialog_title_label": "Otsikko",
+ "prefs_reservations_table_everyone_write_only": "Minä voin julkaista ja tilata, kaikki voivat julkaista",
+ "prefs_appearance_title": "Näkymä",
+ "publish_dialog_topic_reset": "Resetoi topikki",
+ "account_tokens_table_cannot_delete_or_edit": "Nykyistä istuntotunnusta ei voi muokata tai poistaa",
+ "notifications_tags": "Tagit",
+ "prefs_notifications_sound_play": "Toista valittu ääni",
+ "account_tokens_table_last_access_header": "Viimeinen käyty",
+ "action_bar_profile_logout": "Kirjaudu ulos",
+ "publish_dialog_attached_file_filename_placeholder": "Liitetiedoston nimi",
+ "publish_dialog_priority_default": "Oletusprioriteetti",
+ "subscribe_dialog_subscribe_base_url_label": "Palvelimen URL",
+ "account_tokens_table_last_origin_tooltip": "Napsauta IP-osoitteesta {{ip}}, etsiäksesi",
+ "account_usage_reservations_title": "Varatut topikit",
+ "account_upgrade_dialog_tier_price_per_month": "Kuukausi",
+ "message_bar_show_dialog": "Näytä julkaisu dialogi",
+ "publish_dialog_chip_attach_url_label": "Liitä tiedosto URL-osoitteen mukaan",
+ "account_usage_calls_none": "Tällä tilillä ei voi soittaa puheluita",
+ "notifications_click_open_button": "Avaa linkki",
+ "account_tokens_table_current_session": "Nykyinen selainistunto",
+ "account_upgrade_dialog_button_pay_now": "Maksa nyt ja tilaa",
+ "nav_upgrade_banner_description": "Varaa aiheita, lisää viestejä ja sähköposteja, sekä suurempia liitteitä",
+ "publish_dialog_call_reset": "Poista puhelu",
+ "publish_dialog_other_features": "Muut ominaisuudet:",
+ "subscribe_dialog_subscribe_use_another_label": "Käytä toista palvelinta",
+ "reservation_delete_dialog_action_delete_title": "Poista välimuistissa olevat viestit ja liitteet",
+ "signup_error_username_taken": "Käyttäjätunnus {{username}} on jo varattu",
+ "account_basics_phone_numbers_dialog_code_label": "Vahvistuskoodi",
+ "nav_button_subscribe": "Tilaa aihe",
+ "publish_dialog_topic_label": "Topikin nimi",
+ "reservation_delete_dialog_action_delete_description": "Välimuistissa olevat viestit ja liitteet poistetaan pysyvästi. Tätä toimintoa ei voi kumota.",
+ "alert_grant_button": "Myönnä nyt",
+ "account_basics_tier_paid_until": "Tilaus maksettu {{date}} asti, ja se uusitaan automaattisesti",
+ "account_usage_attachment_storage_description": "{{tiedostokoko}} per tiedosto, poistettu {{expiry}} jälkeen",
+ "publish_dialog_chip_click_label": "Napsauta URL-osoitetta",
+ "prefs_notifications_delete_after_one_month_description": "Ilmoitukset poistetaan automaattisesti kuukauden kuluttua",
+ "common_cancel": "Peruuta",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Soita minulle",
+ "signup_already_have_account": "Onko sinulla jo tili ? Kirjaudu sisään !",
+ "publish_dialog_call_item": "Soita puhelinnumeroon {{number}}",
+ "nav_button_account": "Tili",
+ "publish_dialog_click_reset": "Poista napsautettava URL-osoite",
+ "login_title": "Kirjaudu sisään ntfy-tilillesi",
+ "notifications_list": "Ilmoitusluettelo",
+ "common_save": "Tallenna",
+ "prefs_users_dialog_base_url_label": "Palvelin URL, esim. https://ntfy.sh",
+ "account_usage_emails_title": "Sähköpostit lähetetty",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "action_bar_reservation_add": "Varalla oleva aihe",
+ "account_upgrade_dialog_tier_selected_label": "Valittu",
+ "account_upgrade_dialog_button_update_subscription": "Päivitä tilaus",
+ "notifications_attachment_file_video": "videotiedosto",
+ "priority_high": "korkea",
+ "notifications_priority_x": "Prioriteetti {{priority}}",
+ "account_delete_dialog_billing_warning": "Tilin poistaminen peruuttaa myös laskutustilauksesi välittömästi. Et voi enää käyttää laskutuksen hallintapaneelia.",
+ "prefs_notifications_min_priority_description_max": "Näytä ilmoitukset, jos prioriteetti on 5 (max)",
+ "subscribe_dialog_login_description": "Tämä Topikki on suojattu salasanalla. Anna käyttäjätunnus ja salasana.",
+ "account_upgrade_dialog_reservations_warning_other": "Valittu taso sallii vähemmän varattuja topikkeja kuin nykyinen tasosi. Ennen kuin muutat tasosi, poista vähintään {{count}} varausta. Voit poistaa varauksia Asetuksista.",
+ "prefs_users_dialog_title_add": "Lisää käyttäjä",
+ "account_tokens_dialog_button_create": "Luo tunnus",
+ "nav_button_settings": "Asetukset",
+ "publish_dialog_priority_min": "Min. etusijalla",
+ "account_tokens_table_create_token_button": "Luo käyttöoikeustunnus",
+ "notifications_delete": "Poista",
+ "notifications_actions_not_supported": "Toimintoa ei tueta verkkosovelluksessa",
+ "notifications_actions_open_url_title": "Siirry osoitteeseen {{url}}",
+ "notifications_none_for_any_title": "Et ole saanut ilmoituksia.",
+ "notifications_none_for_topic_description": "Jos haluat lähettää ilmoituksia tähän topikkiin, PUT tai POST topikin URL-osoitteeseen.",
+ "notifications_none_for_any_description": "Jos haluat lähettää ilmoituksia topikkiin, PUT tai POST topikin URL-osoitteeseen. Tässä on esimerkki yhden topikin käyttämisestä.",
+ "notifications_no_subscriptions_title": "Näyttää siltä, että sinulla ei ole vielä tilauksia.",
+ "notifications_none_for_topic_title": "Et ole vielä saanut ilmoituksia tästä aiheesta.",
+ "notifications_actions_http_request_title": "Lähetä HTTP {{method}} to {{url}}",
+ "reserve_dialog_checkbox_label": "Käänteinen aihe ja aseta pääsy",
+ "publish_dialog_progress_uploading": "Lähetetään …",
+ "publish_dialog_title_no_topic": "Julkaise ilmoitus",
+ "notifications_example": "Esimerkki",
+ "notifications_loading": "Ladataan ilmoituksia …",
+ "notifications_no_subscriptions_description": "Klikkaa \"{{linktext}}\" linkkiä luodaksesi tai tilataksesi aihe. Sen jälkeen voit lähettää viestejä PUT tai POST metodeilla ja saat ilmoituksesi täällä.",
+ "display_name_dialog_description": "Aseta vaihtoehtoinen nimi aiheelle, joka on näytetty tilaus-listassa. Tämä auttaa tunnistamaan aiheet helpommin, joilla on hankalat nimet.",
+ "publish_dialog_message_published": "Ilmoitus julkaistu",
+ "notifications_more_details": "Saadaksesi lisää tietoa, katso nettisivu tai documentointi.",
+ "publish_dialog_attachment_limits_quota_reached": "ylittää kiintiön, {{remainingBytes}} jäljellä",
+ "publish_dialog_title_topic": "Julkaise aiheeseen {{topic}}",
+ "display_name_dialog_placeholder": "Näyttönimi",
+ "publish_dialog_attachment_limits_file_and_quota_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan ja määrän, {{remainingBytes}} jäljellä",
+ "publish_dialog_attachment_limits_file_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan",
+ "publish_dialog_progress_uploading_detail": "Lähetetään {{loaded}}/{{total}} ({{percent}}%) …",
+ "display_name_dialog_title": "Vaihda näyttönimi"
+}
diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json
index a0d6a1b4..096b62af 100644
--- a/web/public/static/langs/fr.json
+++ b/web/public/static/langs/fr.json
@@ -272,7 +272,7 @@
"account_delete_dialog_button_submit": "Supprimer définitivement le compte",
"account_delete_dialog_billing_warning": "Supprimer votre compte annule aussi immédiatement votre facturation. Vous n'aurez plus accès à votre tableau de bord de facturation.",
"account_upgrade_dialog_title": "Changer le tarif du compte",
- "account_upgrade_dialog_proration_info": "Facturation : Lors d'un changement entre un plan payant et un autre, la différence de prix sera créditée ou remboursée sur la prochaine facture. Vous ne recevrez pas d'autre facture avant la fin de la prochaine période de facturation.",
+ "account_upgrade_dialog_proration_info": "Facturation : Lors d'un changement vers un tiers payant, la différence de prix sera débitée immédiatement. En passant d'un tiers payant a gratuit, votre solde sera utilisé pour payer de futur factures.",
"account_upgrade_dialog_reservations_warning_other": "Le tarif sélectionné autorise moins de sujets réservés que votre tarif actuel. Avant de changer de tarif, veuillez supprimer au moins {{count}} sujets réservés. Vous pouvez supprimer des sujets réservés dans les Paramètres.",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} sujets réservés",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers",
@@ -368,8 +368,17 @@
"account_basics_phone_numbers_dialog_code_placeholder": "Ex : 123456",
"account_basics_phone_numbers_dialog_check_verification_button": "Code de confirmarion",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
- "account_basics_phone_numbers_dialog_channel_call": "Appel",
+ "account_basics_phone_numbers_dialog_channel_call": "Appeler",
"account_usage_calls_none": "Aucun appels téléphoniques ne peut être fait avec ce compte",
"publish_dialog_call_reset": "Supprimer les appels téléphoniques",
- "publish_dialog_chip_call_label": "Appel téléphonique"
+ "publish_dialog_chip_call_label": "Appel téléphonique",
+ "account_upgrade_dialog_tier_features_messages_one": "{{messages}} message journalier",
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} mail journalier",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} appels journaliers",
+ "account_upgrade_dialog_tier_features_no_calls": "Aucun appel",
+ "publish_dialog_call_item": "Appeler le numéro {{number}}",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Aucun numéro de téléphone vérifié",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} sujet réservé",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} appels journaliers",
+ "account_usage_calls_title": "Appels téléphoniques passés"
}
diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json
index 0967ef42..92d35610 100644
--- a/web/public/static/langs/gl.json
+++ b/web/public/static/langs/gl.json
@@ -1 +1,384 @@
-{}
+{
+ "common_cancel": "Cancelar",
+ "common_save": "Gardar",
+ "common_add": "Engadir",
+ "signup_disabled": "O rexistro está desactivado",
+ "signup_error_username_taken": "O identificador {{username}} xa está collido",
+ "login_title": "Accede á túa conta ntfy",
+ "action_bar_send_test_notification": "Enviar notificación de proba",
+ "action_bar_clear_notifications": "Limpar todas as notificacións",
+ "action_bar_unsubscribe": "Retirar subscrición",
+ "action_bar_profile_settings": "Axustes",
+ "message_bar_type_message": "Escribe aquí a mensaxe",
+ "notifications_copied_to_clipboard": "Copiada ao portapapeis",
+ "notifications_attachment_image": "Imaxe anexa",
+ "notifications_attachment_copy_url_title": "Copiar URL do anexo ao portapapeis",
+ "notifications_attachment_copy_url_button": "Copiar URL",
+ "notifications_attachment_open_title": "Ir a {{url}}",
+ "notifications_attachment_file_audio": "ficheiro de audio",
+ "notifications_attachment_file_app": "ficheiro de app Android",
+ "notifications_attachment_file_document": "outro documento",
+ "notifications_click_copy_url_title": "Copiar URL da ligazón ao portapapeis",
+ "notifications_click_copy_url_button": "Copiar ligazón",
+ "notifications_actions_open_url_title": "Ir a {{url}}",
+ "notifications_none_for_topic_description": "Para enviar notificacións a este tema, simplemente usa PUT ou POST co URL do tema.",
+ "notifications_no_subscriptions_description": "Preme en \"{{linktext}} para crear ou subscribirte a un tema. Após, podes enviar mensaxes vía PUT ou POST e recibirás aquí as notificacións.",
+ "display_name_dialog_description": "Establecer un nome alternativo para o tema que será mostrado na lista de subscrición. Isto axudará a identificar os temas que teñan nomes complicados.",
+ "publish_dialog_tags_label": "Etiquetas",
+ "publish_dialog_tags_placeholder": "Lista de etiquetas separadas por vírgulas, ex. aviso, tarefa1",
+ "publish_dialog_priority_label": "Prioridade",
+ "publish_dialog_click_label": "URL a premer",
+ "publish_dialog_click_placeholder": "URL que se abre ao premer na notificación",
+ "publish_dialog_click_reset": "Desbotar o URL a premer",
+ "common_back": "Atrás",
+ "common_copy_to_clipboard": "Copiar ao portapapeis",
+ "signup_title": "Crear unha conta ntfy",
+ "signup_form_username": "Identificador",
+ "signup_form_password": "Contrasinal",
+ "signup_form_confirm_password": "Confirmar contrasinal",
+ "signup_form_button_submit": "Crear conta",
+ "login_form_button_submit": "Acceder",
+ "login_link_signup": "Crear conta",
+ "login_disabled": "O acceso está desactivado",
+ "action_bar_show_menu": "Mostrar menú",
+ "action_bar_toggle_mute": "Acalar/Reactivar as notificacións",
+ "message_bar_error_publishing": "Erro ao publicar a notificación",
+ "message_bar_publish": "Publicar mensaxe",
+ "nav_topics_title": "Temas subscritos",
+ "nav_button_documentation": "Documentación",
+ "nav_button_publish_message": "Publicar notificación",
+ "nav_button_subscribe": "Subscribirse ao tema",
+ "nav_button_muted": "Notificacións acaladas",
+ "nav_button_connecting": "conectando",
+ "nav_upgrade_banner_label": "Mellorar a ntfy Pro",
+ "alert_not_supported_description": "O teu navegador non ten soporte para notificacións.",
+ "notifications_priority_x": "Prioridade {{priority}}",
+ "notifications_attachment_link_expires": "a ligazón caduca o {{date}}",
+ "notifications_attachment_link_expired": "a ligazón de descarga caducou",
+ "notifications_attachment_file_image": "ficheiro de imaxe",
+ "notifications_attachment_file_video": "ficheiro de vídeo",
+ "notifications_actions_not_supported": "Acción non soportada na aplicación web",
+ "notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}",
+ "notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.",
+ "reserve_dialog_checkbox_label": "Reservar tema e configurar acceso",
+ "notifications_loading": "Cargando notificacións…",
+ "publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com",
+ "publish_dialog_topic_label": "Nome do tema",
+ "publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo",
+ "publish_dialog_topic_reset": "Restablecer tema",
+ "publish_dialog_title_label": "Título",
+ "publish_dialog_title_placeholder": "Título das notificacións, ex. Alerta de reunión",
+ "publish_dialog_message_label": "Mensaxe",
+ "publish_dialog_message_placeholder": "Escribe aquí a mensaxe",
+ "publish_dialog_email_label": "Correo electrónico",
+ "signup_form_toggle_password_visibility": "Cambiar visibilidade do contrasinal",
+ "signup_already_have_account": "Xa tes unha conta? Accede!",
+ "signup_error_creation_limit_reached": "Acadouse o límite de creación de contas",
+ "action_bar_logo_alt": "logo ntfy",
+ "action_bar_settings": "Axustes",
+ "action_bar_account": "Conta",
+ "action_bar_change_display_name": "Cambiar nome público",
+ "action_bar_reservation_add": "Reservar tema",
+ "action_bar_reservation_edit": "Cambiar a reserva",
+ "action_bar_reservation_delete": "Desbotar a reserva",
+ "action_bar_reservation_limit_reached": "Acadouse o límite",
+ "action_bar_toggle_action_menu": "Abrir/Pechar menú de accións",
+ "action_bar_profile_title": "Perfil",
+ "action_bar_profile_logout": "Pechar sesión",
+ "action_bar_sign_in": "Acceder",
+ "action_bar_sign_up": "Crear conta",
+ "message_bar_show_dialog": "Mostrar diálogo para publicar",
+ "nav_button_all_notifications": "Todas as notificacións",
+ "nav_button_account": "Conta",
+ "nav_button_settings": "Axustes",
+ "nav_upgrade_banner_description": "Reserva temas, máis mensaxes e correos electrónicos así como anexos máis grandes",
+ "alert_grant_title": "As notificacións están desactivadas",
+ "alert_grant_description": "Concede permiso no navegador para mostrar notificacións de escritorio.",
+ "alert_grant_button": "Conceder agora",
+ "alert_not_supported_title": "Non hai soporte para notificacións",
+ "alert_not_supported_context_description": "Só hai soporte para notificacións ao usar HTTPS. Esta é unha limitación da API de Notificacións.",
+ "notifications_list": "Lista de notificacións",
+ "notifications_list_item": "Notificación",
+ "notifications_mark_read": "Marcar como lida",
+ "notifications_delete": "Eliminar",
+ "notifications_tags": "Etiquetas",
+ "notifications_new_indicator": "Nova notificación",
+ "notifications_attachment_open_button": "Abrir anexo",
+ "notifications_click_open_button": "Abrir ligazón",
+ "notifications_none_for_any_title": "Non recibiches ningunha notificación.",
+ "notifications_none_for_any_description": "Para enviar notificacións ao tema, simplemente usa PUT ou POST ao URL do tema. Aquí tes un exemplo usando un dos teus temas.",
+ "notifications_no_subscriptions_title": "Semella que aínda non tes subscricións.",
+ "notifications_example": "Exemplo",
+ "display_name_dialog_title": "Cambiar nonme público",
+ "display_name_dialog_placeholder": "Nome público",
+ "publish_dialog_title_topic": "Publicar en {{topic}}",
+ "publish_dialog_title_no_topic": "Publicar notificación",
+ "publish_dialog_progress_uploading": "Enviando…",
+ "publish_dialog_progress_uploading_detail": "Enviando {{loaded}}/{{total}} ({{percent}}%) …",
+ "publish_dialog_message_published": "Notificación publicada",
+ "publish_dialog_attachment_limits_file_and_quota_reached": "supera o límite de ficheiros e cota {{fileSizeLimit}}, quedan {{remainingBytes}}",
+ "publish_dialog_attachment_limits_file_reached": "supera o límite para ficheiros {{fileSizeLimit}}",
+ "publish_dialog_attachment_limits_quota_reached": "supera a cota, quedan {{remainingBytes}}",
+ "publish_dialog_emoji_picker_show": "Elixe emoji",
+ "publish_dialog_priority_min": "Prioridade Mínima",
+ "publish_dialog_priority_low": "Prioridade baixa",
+ "publish_dialog_priority_default": "Prioridade por defecto",
+ "publish_dialog_priority_high": "Prioridade alta",
+ "publish_dialog_priority_max": "Prioridade Máxima",
+ "publish_dialog_base_url_label": "URL do servizo",
+ "notifications_more_details": "Para máis información, visita o sitio web ou le a documentación.",
+ "publish_dialog_call_label": "Chamada de teléfono",
+ "publish_dialog_call_reset": "Retirar chamada de teléfono",
+ "publish_dialog_delay_placeholder": "Adiar a entrega, ex. {{unixTimestamp}}, {{relativeTime}}, ou \"{{naturalLanguage}}\" (Só en inglés)",
+ "publish_dialog_other_features": "Outras características:",
+ "publish_dialog_chip_click_label": "Premer en URL",
+ "publish_dialog_chip_email_label": "Reenvío por correo",
+ "publish_dialog_chip_call_label": "Chamada de teléfono",
+ "publish_dialog_chip_attach_url_label": "Anexar ficheiro por URL",
+ "publish_dialog_button_cancel_sending": "Cancelar o envío",
+ "publish_dialog_button_cancel": "Cancelar",
+ "publish_dialog_button_send": "Enviar",
+ "publish_dialog_attached_file_title": "Ficheiro anexo:",
+ "publish_dialog_attached_file_filename_placeholder": "Nome do ficheiro anexo",
+ "publish_dialog_drop_file_here": "Soltar aquí o ficheiro",
+ "emoji_picker_search_placeholder": "Buscar emoji",
+ "subscribe_dialog_subscribe_title": "Subscribirse a un tema",
+ "publish_dialog_call_item": "Número de teléfono {{number}}",
+ "publish_dialog_email_placeholder": "Enderezo ao que reenviar a notificación, ex. xoana@exemplo.com",
+ "publish_dialog_email_reset": "Retirar reenvío ao correo",
+ "publish_dialog_attach_label": "URL do anexo",
+ "publish_dialog_attach_placeholder": "Anexa un ficheiro por URL, ex. https://f-droid.org/F-Droid.apk",
+ "publish_dialog_attach_reset": "Retirar URL do anexo",
+ "publish_dialog_filename_placeholder": "Nome do ficheiro anexo",
+ "publish_dialog_filename_label": "Nome do ficheiro",
+ "publish_dialog_delay_label": "Adiar",
+ "publish_dialog_delay_reset": "Retirar o adiadamento da entrega",
+ "publish_dialog_chip_attach_file_label": "Anexar ficheiro local",
+ "publish_dialog_chip_delay_label": "Entrega adiada",
+ "publish_dialog_chip_topic_label": "Cambiar tema",
+ "publish_dialog_details_examples_description": "Para ver exemplos e unha descrición polo miúdo das ferramentas de envío, le a documentación.",
+ "publish_dialog_checkbox_publish_another": "Publicar outra",
+ "emoji_picker_search_clear": "Limpar busca",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de teléfono non verificados",
+ "publish_dialog_attached_file_remove": "Retirar ficheiro anexo",
+ "account_upgrade_dialog_tier_features_no_calls": "Sen chamadas",
+ "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre pagamentos, contacta con nós directamente.",
+ "account_tokens_dialog_title_create": "Crear token de acceso",
+ "prefs_reservations_dialog_title_edit": "Editar tema reservado",
+ "priority_default": "por defecto",
+ "prefs_notifications_min_priority_title": "Prioridade mínima",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas de teléfono diarias",
+ "account_upgrade_dialog_tier_current_label": "Actual",
+ "account_tokens_table_token_header": "Token",
+ "prefs_notifications_delete_after_never": "Nunca",
+ "prefs_users_description": "Engadir/eliminar usuarias dos temas protexidos. Ten en conta que as credenciais gárdanse na almacenaxe local do navegador.",
+ "subscribe_dialog_subscribe_description": "Os temas poderían non estar proxetidos con contrasinal, así que elixe un nome complicado de adiviñar. Unha vez subscrita, podes PUT/POST notificacións.",
+ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "aforro ata un {{discount}}%",
+ "account_tokens_dialog_label": "Etiqueta, ex. notificación de Radarr",
+ "account_tokens_table_expires_header": "Caducidade",
+ "account_upgrade_dialog_proration_info": "Axuste: ao mellorar a un plan de pagamento superior, a diferencia vaise cobrar inmediatamente. Se degradas a conta a un plan inferior a diferencia usarase para pagar futuros períodos de pagamento.",
+ "prefs_reservations_dialog_access_label": "Acceso",
+ "account_usage_attachment_storage_title": "Almacenaxe dos anexos",
+ "prefs_users_dialog_username_label": "Identificador, ex. xoana",
+ "prefs_reservations_table_not_subscribed": "Non subscrita",
+ "account_upgrade_dialog_tier_features_emails_other": "{{emails}} correos diarios",
+ "prefs_notifications_min_priority_max_only": "Só prioridade máxima",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas de teléfono diarias",
+ "prefs_notifications_sound_description_some": "As notificacións sonan co ton {{sound}} ao chegar",
+ "prefs_reservations_edit_button": "Editar acceso ao tema",
+ "account_tokens_dialog_expires_never": "O token non caduca",
+ "subscribe_dialog_login_title": "Require inciar sesión",
+ "account_tokens_dialog_expires_x_days": "O token caduca en {{days}} días",
+ "prefs_reservations_table_everyone_read_only": "Podo publicar e subscribirme, calquera pode subscribirse",
+ "prefs_reservations_table_everyone_deny_all": "Só eu podo publicar e subscribirme",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado",
+ "subscribe_dialog_login_button_login": "Acceder",
+ "account_upgrade_dialog_tier_features_no_reservations": "Sen temas reservados",
+ "prefs_users_table_cannot_delete_or_edit": "Non se pode eliminar ou editar unha usuaria coa sesión iniciada",
+ "prefs_notifications_delete_after_three_hours_description": "As notificacións autoelimínanse após tres horas",
+ "prefs_notifications_delete_after_three_hours": "Após tres horas",
+ "prefs_notifications_min_priority_description_x_or_higher": "Mostrar as notificacións se a prioridade é {{number}} {{name}} ou superior",
+ "reservation_delete_dialog_description": "Ao eliminar a reserva cedes a propiedade do tema, e permites que outras persoas poidan reservalo. Podes manter ou eliminar as mensaxes e anexos existentes.",
+ "prefs_reservations_table_everyone_read_write": "Calquera pode publicar e subscribirse",
+ "prefs_reservations_dialog_title_delete": "Eliminar a reserva do tema",
+ "prefs_users_table": "Táboa de usuarias",
+ "prefs_reservations_table_topic_header": "Tema",
+ "reservation_delete_dialog_submit_button": "Eliminar a reserva",
+ "prefs_reservations_limit_reached": "Acadaches o límite de temas que podes reservar.",
+ "account_upgrade_dialog_interval_monthly": "Mensual",
+ "prefs_users_add_button": "Engadir usuaria",
+ "account_upgrade_dialog_tier_features_messages_other": "{{messages}} mensaxes diarias",
+ "prefs_appearance_language_title": "Idioma",
+ "prefs_notifications_delete_after_one_day_description": "As notificacións autoelimínanse após un día",
+ "account_tokens_table_never_expires": "Non caduca",
+ "account_tokens_delete_dialog_title": "Desbotar token de acceso",
+ "prefs_notifications_delete_after_one_month": "Após un mes",
+ "account_tokens_delete_dialog_description": "Antes de borrar o token de acceso mira que ningunha aplicación ou programa o está usando. Esta acción non pode desfacerse.",
+ "account_upgrade_dialog_button_cancel": "Cancelar",
+ "account_tokens_table_label_header": "Etiqueta",
+ "account_upgrade_dialog_billing_contact_website": "Para preguntas sobre pagamentos, vai ao noso sitiio web.",
+ "prefs_notifications_delete_after_never_description": "As notificacións non se eliminarán nunca automáticamente",
+ "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} temas reservados",
+ "prefs_notifications_sound_description_none": "As notificacións non reproducen un ton ao chegar",
+ "account_tokens_description": "Usar tokens de acceso ao publicar e subscribirte a través da API de ntfy, así non tes que enviar as credenciais. Le a documentación para saber máis.",
+ "prefs_reservations_table": "Táboa cos temas reservados",
+ "account_upgrade_dialog_button_cancel_subscription": "Cancelar subscrición",
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo diario",
+ "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por ficheiro",
+ "prefs_reservations_description": "Podes reservar nomes de temas para uso personal. Ao reservar un tema tes a propiedade sobre del, e permíteche definir os permisos de acceso para outras usuarias sobre o tema.",
+ "prefs_users_description_no_sync": "Usuarias e contrasinais non están sincronizados coa túa conta.",
+ "account_tokens_dialog_title_edit": "Editar token de acceso",
+ "prefs_users_table_base_url_header": "URL do servizo",
+ "account_upgrade_dialog_tier_features_messages_one": "{{mensaxes}} mensaxe diaria",
+ "account_upgrade_dialog_reservations_warning_one": "O nivel seleccionado permite reservar menos temas que o nivel actual. Antes de cambiar de nivel, elimina unha reserva polo menos. Podes eliminar as reservas nos Axustes.",
+ "prefs_users_table_user_header": "Usuaria",
+ "error_boundary_stack_trace": "Trazas do problema",
+ "prefs_users_dialog_password_label": "Contrasinal",
+ "prefs_notifications_delete_after_one_week": "Após unha semana",
+ "prefs_reservations_delete_button": "Restablecer acceso ao tema",
+ "prefs_notifications_delete_after_one_week_description": "As notificacións autoelimínanse após unha semana",
+ "error_boundary_unsupported_indexeddb_description": "A app ntfy web precisa a función IndexedDB, e o teu navegador non ten soporte para IndexedDB no modo privado.
Aínda que é unha mágoa, tampouco ten moito senso usar a app ntfy web en modo privado, porque todo se garda na almacenaxe do navegador. Podes aprender máis sobre isto neste tema de GitHub, ou comentarnos o que che parece en Discord ou Matrix.",
+ "subscribe_dialog_subscribe_button_cancel": "Cancelar",
+ "account_basics_tier_description": "O nivel da túa conta",
+ "prefs_reservations_dialog_title_add": "Reservar tema",
+ "account_upgrade_dialog_cancel_warning": "Isto vai cancelar a túa subscrición, e degradar a túa conta o {{date}}. Nesa data, as reservas de temas así como as mensaxes na caché do servidor van ser eliminadas.",
+ "prefs_notifications_sound_title": "Ton da notificación",
+ "prefs_notifications_min_priority_default_and_higher": "Prioridade por defecto e superior",
+ "prefs_reservations_table_access_header": "Acceso",
+ "account_tokens_table_copied_to_clipboard": "Copiouse o token de acceso",
+ "account_tokens_dialog_expires_x_hours": "O token caduca en {{hours}} horas",
+ "prefs_users_edit_button": "Editar usuaria",
+ "account_upgrade_dialog_title": "Cambiar facturación da conta",
+ "priority_low": "baixa",
+ "prefs_reservations_table_click_to_subscribe": "Preme para subscribirte",
+ "error_boundary_description": "Isto non debería pasar. Lamentámolo. Se tes un minuto, informa en GitHub, ou fáinolo saber en Discord ou Matrix.",
+ "priority_min": "min",
+ "prefs_notifications_min_priority_description_any": "Mostrar todas as notificacións, obviando a prioridade",
+ "error_boundary_gathering_info": "Obter máis info…",
+ "error_boundary_unsupported_indexeddb_title": "Non hai soporte para a navegación privada",
+ "prefs_notifications_delete_after_one_day": "Após un día",
+ "error_boundary_title": "vaite!, ntfy fallou",
+ "reservation_delete_dialog_action_keep_description": "As mensaxes e anexos que están no servidor serán visibles públicamente para quen saiba o nome do tema.",
+ "prefs_reservations_add_button": "Engadir tema reservado",
+ "prefs_reservations_title": "Temas reservados",
+ "prefs_reservations_dialog_description": "Ao reservar un tema tes a propiedade sobre el, e permíteche definir os permisos de acceso para outras usuarias.",
+ "account_tokens_delete_dialog_submit_button": "Eliminar definitivamente o token",
+ "prefs_notifications_title": "Notificacións",
+ "account_tokens_title": "Tokens de acceso",
+ "prefs_reservations_dialog_topic_label": "Tema",
+ "prefs_users_title": "Xestionar usuarias",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} anual. Pagamento mensual.",
+ "account_tokens_dialog_expires_unchanged": "Deixar a data de caducidade sen cambiar",
+ "error_boundary_button_copy_stack_trace": "Copiar trazas do problema",
+ "account_tokens_dialog_title_delete": "Eliminar token de acceso",
+ "reservation_delete_dialog_action_keep_title": "Manter as mensaxes e anexos gardados",
+ "prefs_notifications_sound_no_sound": "Sen ton",
+ "account_upgrade_dialog_interval_yearly": "Anual",
+ "account_upgrade_dialog_button_redirect_signup": "Crea unha conta",
+ "account_tokens_dialog_button_cancel": "Cancelar",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Aforro {{save}}.",
+ "prefs_notifications_min_priority_high_and_higher": "Prioridade alta e superior",
+ "priority_max": "máx",
+ "prefs_users_delete_button": "Eliminar usuaria",
+ "prefs_notifications_min_priority_any": "Calquera prioridade",
+ "account_tokens_dialog_expires_label": "O token caduca o",
+ "prefs_notifications_delete_after_title": "Desbotar notificacións",
+ "account_upgrade_dialog_interval_yearly_discount_save": "aforro {{discount}}%",
+ "prefs_users_dialog_title_edit": "Editar usuaria",
+ "prefs_notifications_min_priority_low_and_higher": "Prioridade baixa e superior",
+ "account_tokens_dialog_button_update": "Actualizar token",
+ "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} almacenaxe total",
+ "prefs_reservations_table_everyone_write_only": "Podo publicar e subscribirme, calquera pode publicar",
+ "prefs_appearance_title": "Aparencia",
+ "account_tokens_table_cannot_delete_or_edit": "Non se pode editar ou desbotar o token da sesión actual",
+ "prefs_notifications_sound_play": "Reproducir ton seleccionado",
+ "account_tokens_table_last_access_header": "Último acceso",
+ "account_tokens_table_last_origin_tooltip": "Desde o enderezo IP {{ip}}, preme para detalles",
+ "account_upgrade_dialog_tier_price_per_month": "mes",
+ "account_tokens_table_current_session": "Sesión do navegador actual",
+ "account_upgrade_dialog_button_pay_now": "Paga e subscríbete",
+ "reservation_delete_dialog_action_delete_title": "Eliminar mensaxes e anexos gardados",
+ "reservation_delete_dialog_action_delete_description": "As mensaxes e anexos vanse borrar definitivamente. Esta acción non ten volta.",
+ "prefs_notifications_delete_after_one_month_description": "As notificacións autoelimínanse após un mes",
+ "prefs_users_dialog_base_url_label": "URL do servizo, ex. https://ntfy.sh",
+ "account_upgrade_dialog_tier_selected_label": "Seleccionado",
+ "account_upgrade_dialog_button_update_subscription": "Actualizar subscrición",
+ "priority_high": "alta",
+ "account_delete_dialog_billing_warning": "Ao eliminar a conta tamén cancelas o pagamento das subscricións. Non poderás volver acceder ao taboleiro de pagamentos.",
+ "prefs_notifications_min_priority_description_max": "Mostrar notificacións se a prioridade é 5 (máx)",
+ "account_upgrade_dialog_reservations_warning_other": "O nivel seleccionado permite reservar menos temas que o nivel actual. Antes de cambiar de nivel, elimina {{count}} reservas polo menos. Podes eliminar as reservas nos Axustes.",
+ "prefs_users_dialog_title_add": "Engadir usuaria",
+ "account_tokens_dialog_button_create": "Crear token",
+ "account_tokens_table_create_token_button": "Crear token de acceso",
+ "account_basics_tier_interval_monthly": "mensual",
+ "account_basics_tier_canceled_subscription": "A sua suscripción foi cancelada e vostede será degradado a unha conta gratuita o {{date}}.",
+ "account_basics_password_dialog_current_password_incorrect": "Contrasinal incorrecto",
+ "account_basics_phone_numbers_dialog_number_label": "Número de teléfono",
+ "account_basics_password_dialog_button_submit": "Modificar contrasinal",
+ "account_basics_username_title": "Usuario",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmación",
+ "account_usage_messages_title": "Mesaxes publicados",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS",
+ "account_basics_tier_change_button": "Cambiar",
+ "account_basics_phone_numbers_dialog_description": "Para usar a característica de chamadas de teléfono, vostede debe engadir e verificar ao menos un número de teléfono. A verificación pode ser realizada vía SMS ou a través de chamada.",
+ "account_delete_title": "Borrar conta",
+ "account_delete_dialog_label": "Contrasinal",
+ "account_basics_tier_admin_suffix_with_tier": "(con tier {{tier}})",
+ "subscribe_dialog_login_username_label": "Nome de usuario, ex. phil",
+ "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} non autorizado",
+ "account_basics_title": "Conta",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Aínda non hay números de teléfono",
+ "subscribe_dialog_subscribe_button_generate_topic_name": "Xerar nome",
+ "subscribe_dialog_login_password_label": "Contrasinal",
+ "subscribe_dialog_subscribe_button_subscribe": "Subscribirse",
+ "account_basics_phone_numbers_dialog_title": "Engadir número de teléfono",
+ "account_basics_username_admin_tooltip": "É vostede Admin",
+ "account_delete_dialog_description": "Isto borrará permanentemente a túa conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu nome de usuario non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirme co seu contrasinal na caixa inferior.",
+ "account_usage_reservations_none": "Non hai temas reservados para esta conta",
+ "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. phil_alertas",
+ "account_usage_title": "Uso",
+ "account_basics_tier_upgrade_button": "Mexorar a Pro",
+ "subscribe_dialog_error_topic_already_reserved": "Tema xa reservado",
+ "account_basics_tier_admin_suffix_no_tier": "(sen tier)",
+ "account_basics_tier_payment_overdue": "O pago está retrasado. Por favor, revise o seu método de pago o a súa conta será degradada pronto.",
+ "account_basics_phone_numbers_description": "Para notificacións telefónicas",
+ "account_basics_tier_free": "De balde",
+ "account_basics_tier_admin": "Admin",
+ "account_delete_dialog_button_cancel": "Cancelar",
+ "account_basics_password_description": "Modificar o contrasinal da conta",
+ "account_usage_calls_title": "Chamadas realizadas",
+ "account_basics_tier_basic": "Básico",
+ "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado no portapapeis",
+ "account_basics_tier_title": "Tipo de conta",
+ "account_usage_cannot_create_portal_session": "Non foi posible abrir o portal de pagos",
+ "account_delete_description": "Borrar permanentemente a túa conta",
+ "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444",
+ "account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456",
+ "account_basics_tier_manage_billing_button": "Xestionar pagos",
+ "account_basics_username_description": "Ei, ese eres ti ❤",
+ "account_basics_password_dialog_confirm_password_label": "Confirmar contrasinal",
+ "account_basics_tier_interval_yearly": "anual",
+ "account_delete_dialog_button_submit": "Borrar permanentemente a conta",
+ "account_basics_phone_numbers_dialog_channel_call": "Chamada",
+ "account_basics_password_title": "Contrasinal",
+ "account_basics_password_dialog_new_password_label": "Novo contrasinal",
+ "account_usage_of_limit": "de {{limit}}",
+ "subscribe_dialog_error_user_anonymous": "anónimo",
+ "account_usage_basis_ip_description": "Estadísticas de uso e límites para esta conta están basados na sua IP, polo que poden estar compartidos con outros usuarios. Os limites mostrados son aproximados, basados nos ratios de limite existentes.",
+ "account_basics_password_dialog_title": "Modificar contrasinal",
+ "account_usage_limits_reset_daily": "Límite de uso é reiniciado diariamente a medianoite (UTC(",
+ "account_usage_unlimited": "Sen límites",
+ "account_basics_phone_numbers_title": "Números de teléfono",
+ "account_basics_password_dialog_current_password_label": "Contrasinal actual",
+ "subscribe_dialog_subscribe_base_url_label": "URL do servizo",
+ "account_usage_reservations_title": "Temas reservados",
+ "account_usage_calls_none": "Non se poden realizar chamadas con esta conta",
+ "subscribe_dialog_subscribe_use_another_label": "Usar outro servidor",
+ "account_basics_phone_numbers_dialog_code_label": "Código de verificación",
+ "account_basics_tier_paid_until": "Suscripción pagada ata {{date}}, e vaise auto-renovar",
+ "account_usage_attachment_storage_description": "{{filesize}} por arquivo, borrado despois de {{expiry}}",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Chámame",
+ "account_usage_emails_title": "Emails enviados",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse."
+}
diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json
index ff731345..fa31ee32 100644
--- a/web/public/static/langs/it.json
+++ b/web/public/static/langs/it.json
@@ -189,7 +189,7 @@
"prefs_notifications_delete_after_three_hours_description": "Le notifiche vengono eliminate automaticamente dopo tre ore",
"error_boundary_unsupported_indexeddb_description": "L'app web ntfy ha bisogno di IndexedDB per funzionare e il tuo browser non supporta IndexedDB in modalità di navigazione privata.
Anche se questo è un peccato, non ha molto senso usare il web ntfy app in modalità di navigazione privata comunque, perché tutto è archiviato nella memoria del browser. Puoi leggere di più a riguardo in questo numero di GitHub o parlarci su Discord o Matrix.",
"nav_upgrade_banner_label": "Passa alla versione Pro di ntfy",
- "alert_not_supported_context_description": "Le Notificche sono supportate solo tramite HTTPS. Questa è una limitazione delle Notifications API.",
+ "alert_not_supported_context_description": "Le Notifiche sono supportate solo tramite HTTPS. Questa è una limitazione delle Notifications API.",
"account_basics_password_dialog_new_password_label": "Nuova password",
"action_bar_profile_logout": "Esci",
"account_basics_tier_interval_monthly": "mensile",
@@ -267,5 +267,45 @@
"publish_dialog_chip_call_label": "Chiamata telefonica",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato",
"account_basics_phone_numbers_title": "Numeri di telefono",
- "account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica."
+ "account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica.",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topic riservato",
+ "account_upgrade_dialog_billing_contact_email": "Per domande di fatturazione, contattaci direttamente.",
+ "account_upgrade_dialog_tier_current_label": "Attuale",
+ "account_basics_phone_numbers_dialog_number_label": "Numero di telefono",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Conferma codice",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Invia SMS",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Ancora nessun numero di telefono",
+ "account_basics_phone_numbers_dialog_title": "Aggiungi un numero di telefono",
+ "account_upgrade_dialog_button_cancel": "Cancella",
+ "account_upgrade_dialog_billing_contact_website": "Per domande di fatturazione, visita per favore in nostro sito.",
+ "account_upgrade_dialog_button_cancel_subscription": "Cancella iscrizione",
+ "account_basics_phone_numbers_description": "Per notifiche via chiamata",
+ "account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti",
+ "account_basics_phone_numbers_dialog_number_placeholder": "p. e. +391234567890",
+ "account_basics_phone_numbers_dialog_code_placeholder": "p. e. 123456",
+ "account_tokens_title": "Token d'accesso",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} all'anno. Addebitato annualmente.",
+ "account_basics_phone_numbers_dialog_channel_call": "Chiama",
+ "account_upgrade_dialog_button_redirect_signup": "Iscriviti ora",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} addebitato annualmente. Risparmia {{save}}.",
+ "account_upgrade_dialog_tier_price_per_month": "mese",
+ "account_upgrade_dialog_button_pay_now": "Paga ora e isciviti",
+ "account_basics_phone_numbers_dialog_code_label": "Codice di verifica",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Chiamami",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_upgrade_dialog_tier_selected_label": "Selezionato",
+ "account_upgrade_dialog_button_update_subscription": "Aggiorna iscrizione",
+ "account_usage_attachment_storage_title": "Archivio allegati",
+ "account_delete_dialog_description": "Il tuo account sarà permanentemente cancellato assieme a tutti i tuoi dati presenti sul server. Dopo la cancellazione, la tua username non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.",
+ "account_delete_dialog_button_cancel": "Annulla",
+ "account_usage_calls_title": "Chiamate effettuate",
+ "account_delete_description": "Elimina permanentemente il tuo account",
+ "account_delete_dialog_button_submit": "Elimina il tuo account permanentemente",
+ "account_usage_basis_ip_description": "Le statistiche di utilizzo e i limiti per questo account sono basati sul tuo indirizzo IP, quindi potrebbero essere in condivisione con altri utenti. I limiti mostrati sopra sono approssimazioni basate sui limiti esistenti.",
+ "account_usage_calls_none": "Questo account non può effettuare chiamate",
+ "account_delete_dialog_billing_warning": "Eliminando il tuo account perderai immediatamente il tuo abbonamento. Non potrai più accedere alla dashboard di fatturazione.",
+ "account_delete_dialog_label": "Password",
+ "account_upgrade_dialog_tier_features_no_reservations": "Nessun argomento riservato",
+ "account_upgrade_dialog_tier_features_messages_one": "{{messages}} messaggi giornalieri",
+ "account_upgrade_dialog_reservations_warning_one": "Il livello selezionato consente meno argomenti riservati rispetto al livello corrente. Prima di cambiare il livello, si prega di eliminare almeno una prenotazione. È possibile rimuovere le prenotazioni nel Impostazioni."
}
diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json
index b03adca7..13cd419e 100644
--- a/web/public/static/langs/nb_NO.json
+++ b/web/public/static/langs/nb_NO.json
@@ -190,5 +190,10 @@
"error_boundary_unsupported_indexeddb_title": "Privat surfing støttes ikke",
"action_bar_account": "Konto",
"action_bar_profile_settings": "Innstillinger",
- "nav_button_account": "Konto"
+ "nav_button_account": "Konto",
+ "signup_title": "Opprett en ntfy konto",
+ "signup_form_username": "Brukernavn",
+ "signup_form_password": "Passord",
+ "signup_form_button_submit": "Meld deg på",
+ "signup_form_confirm_password": "Bekreft passord"
}
diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json
index 8733345c..d8904e01 100644
--- a/web/public/static/langs/pl.json
+++ b/web/public/static/langs/pl.json
@@ -317,5 +317,43 @@
"account_upgrade_dialog_tier_features_emails_many": "{{emails}} maili dziennie",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} wiadomość dziennie",
"account_upgrade_dialog_tier_features_messages_few": "{{messages}} wiadomości dziennie",
- "account_upgrade_dialog_tier_features_messages_many": "{{messages}} wiadomości dziennie"
+ "account_upgrade_dialog_tier_features_messages_many": "{{messages}} wiadomości dziennie",
+ "account_upgrade_dialog_tier_features_no_calls": "Brak połączeń telefonicznych",
+ "account_upgrade_dialog_tier_features_calls_one": "Dzienne rozmowy telefoniczne: {{calls}}",
+ "account_upgrade_dialog_tier_current_label": "Bieżące",
+ "account_basics_phone_numbers_dialog_number_label": "Numer telefonu",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Potwierdź kod",
+ "account_upgrade_dialog_proration_info": "Proporcja: Przy ulepszaniu pomiędzy płatnymi planami, różnica ceny będzie pobrana natychmiast. Przy obniżaniu planu do niższych planów, środki zostaną użyte do rozliczenia przyszłych okresów subskrypcji.",
+ "account_upgrade_dialog_tier_features_emails_other": "Dzienne wiadomości e-mail: {{emails}}",
+ "account_upgrade_dialog_tier_features_calls_other": "Dzienne rozmowy telefoniczne: {{calls}}",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Wyślij SMS",
+ "account_tokens_dialog_expires_never": "Token nigdy nie wygasa",
+ "account_tokens_dialog_expires_x_days": "Token wygasa za {{days}} dni",
+ "account_basics_phone_numbers_dialog_description": "Aby używać funkcji powiadomień telefonicznych, musisz dodać i zweryfikować no najmniej jeden numer telefonu. Weryfikacja może być dokonana przez SMS lub połączenie telefoniczne.",
+ "account_upgrade_dialog_tier_features_messages_other": "Dzienne wiadomości: {{messages}}",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Brak numerów telefonów",
+ "account_tokens_delete_dialog_title": "Usuń token dostępu",
+ "publish_dialog_chip_call_label": "Rozmowa telefoniczna",
+ "account_basics_phone_numbers_dialog_title": "Dodaj numer telefonu",
+ "account_upgrade_dialog_tier_features_reservations_other": "Zarezerwowane tematy: {{reservations}}",
+ "account_upgrade_dialog_reservations_warning_one": "Wybrany plan zezwala na mniejszą liczbę zarezerwowanych tematów niż obecny. Przed zmianą planu, usuń co najmniej jedną rezerwację. Rezerwacje możesz usunąć w Ustawieniach.",
+ "account_basics_phone_numbers_description": "Dla powiadomień telefonicznych",
+ "account_upgrade_dialog_cancel_warning": "To anuluje Twoją subskrypcję i obniży status Twojego konta {{date}}. Tego dnia rezerwacja tematów oraz wiadomości przechowywane na serwerze zostaną usunięte.",
+ "account_tokens_dialog_expires_x_hours": "Token wygasa za {{hours}} godzin(y)",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Brak zweryfikowanych numerów telefonów",
+ "publish_dialog_call_label": "Rozmowa telefoniczna",
+ "account_usage_calls_title": "Wykonane połączenia telefoniczne",
+ "account_basics_phone_numbers_copied_to_clipboard": "Numer telefonu skopiowany do schowka",
+ "account_basics_phone_numbers_dialog_number_placeholder": "np. +1222333444",
+ "account_basics_phone_numbers_dialog_code_placeholder": "np. 123456",
+ "account_basics_phone_numbers_dialog_channel_call": "Zadzwoń",
+ "account_basics_phone_numbers_title": "Numery telefonów",
+ "account_usage_calls_none": "Nie wykonano żadnych połączeń z tego konta",
+ "publish_dialog_call_reset": "Usuń rozmowę telefoniczną",
+ "account_basics_phone_numbers_dialog_code_label": "Kod weryfikacyjny",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Zadzwoń do mnie",
+ "publish_dialog_call_item": "Zadzwoń pod numer {{number}}",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_upgrade_dialog_tier_selected_label": "Wybrane",
+ "account_upgrade_dialog_reservations_warning_other": "Wybrany plan zezwala na mniejszą liczbę zarezerwowanych tematów niż obecny. Przed zmianą planu, usuń co najmniej tyle rezerwacji: {{count}}. Rezerwacje możesz usunąć w Ustawieniach."
}
diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json
index 8452ea2e..4571dd40 100644
--- a/web/public/static/langs/pt_BR.json
+++ b/web/public/static/langs/pt_BR.json
@@ -191,10 +191,97 @@
"error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.
Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso nesta edição do GitHub, ou falar conosco em Discord ou Matrix.",
"action_bar_reservation_add": "Reserve topic",
"action_bar_reservation_edit": "Change reservation",
- "signup_disabled": "Signup is disabled",
- "signup_error_username_taken": "Username {{username}} is already taken",
- "signup_error_creation_limit_reached": "Account creation limit reached",
- "action_bar_reservation_delete": "N",
- "action_bar_account": "Account",
- "action_bar_change_display_name": "Change display name"
+ "signup_disabled": "Registrar está desativado",
+ "signup_error_username_taken": "Usuário {{username}} já existe",
+ "signup_error_creation_limit_reached": "Limite de criação de contas atingido",
+ "action_bar_reservation_delete": "Remover reserva",
+ "action_bar_account": "Conta",
+ "action_bar_change_display_name": "Change display name",
+ "common_copy_to_clipboard": "Copiar para área de transferência",
+ "login_link_signup": "Registrar",
+ "login_title": "Entrar na sua conta ntfy",
+ "login_form_button_submit": "Entrar",
+ "login_disabled": "Login está desabilitado",
+ "action_bar_reservation_limit_reached": "Limite atingido",
+ "action_bar_profile_title": "Perfil",
+ "action_bar_profile_settings": "Configurações",
+ "action_bar_profile_logout": "Sair",
+ "action_bar_sign_in": "Entrar",
+ "action_bar_sign_up": "Registrar",
+ "nav_button_account": "Conta",
+ "signup_title": "Criar uma conta ntfy",
+ "signup_form_username": "Usuário",
+ "signup_form_password": "Senha",
+ "signup_form_confirm_password": "Confirmar senha",
+ "signup_form_button_submit": "Registrar",
+ "account_basics_phone_numbers_title": "Telefones",
+ "signup_form_toggle_password_visibility": "Ativar visibilidade de senha",
+ "signup_already_have_account": "Já possui uma conta? Entrar!",
+ "nav_upgrade_banner_label": "Atualizar para ntfy Pro",
+ "account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.",
+ "account_basics_phone_numbers_description": "Para notificações de chamada telefônica",
+ "account_basics_tier_interval_monthly": "mensal",
+ "account_basics_tier_canceled_subscription": "Sua assinatura foi cancelada e será rebaixada para uma conta gratuita em {{date}}.",
+ "account_basics_password_dialog_current_password_incorrect": "Senha incorreta",
+ "account_basics_phone_numbers_dialog_number_label": "Número de telefone",
+ "account_basics_password_dialog_button_submit": "Alterar senha",
+ "reserve_dialog_checkbox_label": "Guardar tópico e configurar acesso",
+ "account_basics_username_title": "Nome de usuário",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código",
+ "account_usage_attachment_storage_title": "Armazenamento de anexos",
+ "account_usage_messages_title": "Mensagens publicadas",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS",
+ "account_basics_tier_change_button": "Mudar",
+ "account_basics_tier_admin_suffix_with_tier": "(com nível {{tier}})",
+ "account_basics_title": "Conta",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Ainda não há números de telefone",
+ "subscribe_dialog_subscribe_button_generate_topic_name": "Gerar nome",
+ "display_name_dialog_description": "Defina um nome alternativo para o tópico exibido na lista de inscrições. Isso pode ajudar a identificar mais facilmente tópicos com nomes complicados.",
+ "publish_dialog_chip_call_label": "Chamada telefônica",
+ "account_basics_phone_numbers_dialog_title": "Adicionar número de telefone",
+ "account_basics_username_admin_tooltip": "Você é Administrador",
+ "account_usage_reservations_none": "Nenhum tópico reservado para esta conta",
+ "account_usage_title": "Uso",
+ "account_basics_tier_upgrade_button": "Atualizar para Pro",
+ "subscribe_dialog_error_topic_already_reserved": "Tópico já reservado",
+ "account_basics_tier_admin_suffix_no_tier": "(sem nível)",
+ "account_basics_tier_payment_overdue": "O teu pagamento está atrasado. Por favor, atualize seu método de pagamento, ou sua conta será rebaixada em breve.",
+ "account_basics_tier_description": "Nível de poder da sua conta",
+ "account_basics_tier_free": "Grátis",
+ "account_basics_tier_admin": "Administrador",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nenhum número de telefone verificado",
+ "account_basics_password_description": "Alterar a senha da sua conta",
+ "publish_dialog_call_label": "Chamada telefônica",
+ "account_usage_calls_title": "Chamadas de telefone feitas",
+ "account_basics_tier_basic": "Básico",
+ "alert_not_supported_context_description": "Notificações são suportadas apenas através de HTTPS. Esta é uma limitação da API de Notificações.",
+ "account_basics_phone_numbers_copied_to_clipboard": "Número de telefone copiado para a área de transferência",
+ "account_basics_tier_title": "Tipo de conta",
+ "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444",
+ "account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456",
+ "account_basics_tier_manage_billing_button": "Gerenciar faturamento",
+ "account_basics_username_description": "Ei, é você ❤",
+ "account_basics_password_dialog_confirm_password_label": "Confirmar senha",
+ "account_basics_tier_interval_yearly": "anual",
+ "account_basics_phone_numbers_dialog_channel_call": "Ligar",
+ "account_basics_password_title": "Senha",
+ "account_basics_password_dialog_new_password_label": "Nova senha",
+ "display_name_dialog_placeholder": "Nome de exibição",
+ "account_usage_of_limit": "de {{limit}}",
+ "account_basics_password_dialog_title": "Alterar senha",
+ "account_usage_limits_reset_daily": "Os limites de uso são redefinidos diariamente à meia-noite (UTC)",
+ "account_usage_unlimited": "Ilimitado",
+ "account_basics_password_dialog_current_password_label": "Senha atual",
+ "account_usage_reservations_title": "Tópicos reservados",
+ "account_usage_calls_none": "Nenhum telefonema pode ser feito com esta conta",
+ "display_name_dialog_title": "Alterar o nome de exibição",
+ "nav_upgrade_banner_description": "Guarde tópicos, mais mensagens & emails e anexos grandes",
+ "publish_dialog_call_reset": "Remover chamada telefônica",
+ "account_basics_phone_numbers_dialog_code_label": "Código de verificação",
+ "account_basics_tier_paid_until": "Assinatura paga até {{date}}, será renovada automaticamente",
+ "account_usage_attachment_storage_description": "{{filesize}} por arquivo, excluído após {{expiry}}",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Ligar pra mim",
+ "publish_dialog_call_item": "Ligue para o número de telefone {{number}}",
+ "account_usage_emails_title": "Emails enviados",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS"
}
diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json
index bfb90b50..67b92e1d 100644
--- a/web/public/static/langs/ro.json
+++ b/web/public/static/langs/ro.json
@@ -101,5 +101,27 @@
"notifications_click_open_button": "Deschide link",
"publish_dialog_emoji_picker_show": "Alege un emoji",
"notifications_loading": "Încărcare notificări…",
- "publish_dialog_priority_low": "Prioritate joasă"
+ "publish_dialog_priority_low": "Prioritate joasă",
+ "signup_form_username": "Nume de utilizator",
+ "signup_form_button_submit": "Înscrie-te",
+ "common_copy_to_clipboard": "Copiază în clipboard",
+ "signup_form_toggle_password_visibility": "Schimbă vizibilitatea parolei",
+ "signup_title": "Crează un cont ntfy",
+ "signup_already_have_account": "Deja ai un cont? Autentifică-te!",
+ "login_disabled": "Autentificarea este dezactivată",
+ "signup_error_creation_limit_reached": "S-a atins limita de conturi",
+ "action_bar_toggle_action_menu": "Deschide/Închide meniul de acțiuni",
+ "action_bar_sign_up": "Înscriere",
+ "message_bar_publish": "Publică mesajul",
+ "login_link_signup": "Înscrie-te",
+ "action_bar_sign_in": "Autentificare",
+ "action_bar_reservation_edit": "Schimbă rezervarea",
+ "action_bar_reservation_delete": "Șterge rezervarea",
+ "login_form_button_submit": "Autentifică-te",
+ "signup_disabled": "Înscrierea este dezactivată",
+ "action_bar_profile_logout": "Ieșire",
+ "message_bar_show_dialog": "Arată dialogul de publicare",
+ "signup_error_username_taken": "Numele de utilizator {{username}} este deja folosit",
+ "login_title": "Autentifică-te în contul ntfy",
+ "action_bar_reservation_add": "Rezervă topicul"
}
diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json
index 437b6aeb..71cef5a4 100644
--- a/web/public/static/langs/ru.json
+++ b/web/public/static/langs/ru.json
@@ -352,5 +352,33 @@
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} в год. Оплата помесячно.",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ежегодно. Сэкономьте {{save}}.",
"account_upgrade_dialog_billing_contact_email": "По вопросам оплаты, пожалуйста свяжитесь с нами.",
- "account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему сайту."
+ "account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему сайту.",
+ "publish_dialog_call_reset": "Удалить вызов",
+ "account_basics_phone_numbers_dialog_description": "Для того что бы использовать возможность уведомлений о вызовах, нужно добавить и проверить хотя бы один номер телефона. Проверить можно используя SMS или звонок.",
+ "account_basics_phone_numbers_dialog_title": "Добавить номер телефона",
+ "account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444",
+ "account_basics_phone_numbers_dialog_code_placeholder": "например 123456",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS",
+ "account_usage_calls_title": "Совершённые вызовы",
+ "account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров",
+ "account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена",
+ "account_upgrade_dialog_tier_features_no_calls": "Нет вызовов",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} ежедневный звонок",
+ "account_basics_phone_numbers_dialog_number_label": "Номер телефона",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Подтвердить код",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} ежедневных звонков",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервированная тема",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Телефонных номеров пока нет",
+ "publish_dialog_chip_call_label": "Звонок",
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} ежедневное письмо",
+ "account_upgrade_dialog_tier_features_messages_one": "{{messages}} ежедневное сообщения",
+ "account_basics_phone_numbers_description": "Для уведомлений о телефонных звонках",
+ "publish_dialog_call_label": "Звонок",
+ "account_basics_phone_numbers_dialog_channel_call": "Позвонить",
+ "account_basics_phone_numbers_title": "Номера телефонов",
+ "account_basics_phone_numbers_dialog_code_label": "Проверочный код",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Позвонить мне",
+ "publish_dialog_call_item": "Вызов телефонного номера {{number}}",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS"
}
diff --git a/web/public/static/langs/sk.json b/web/public/static/langs/sk.json
new file mode 100644
index 00000000..0e3f57a7
--- /dev/null
+++ b/web/public/static/langs/sk.json
@@ -0,0 +1,384 @@
+{
+ "common_save": "Uložiť",
+ "common_back": "Späť",
+ "common_copy_to_clipboard": "Kopírovať do schránky",
+ "signup_title": "Vytvoriť ntfy účet",
+ "signup_form_username": "Používateľské meno",
+ "signup_form_confirm_password": "Potvrdenie hesla",
+ "signup_form_button_submit": "Zaregistrovať sa",
+ "signup_form_toggle_password_visibility": "Prepnúť viditeľnosť hesla",
+ "signup_error_username_taken": "Používateľské meno {{username}} je už obsadené",
+ "login_form_button_submit": "Prihlásiť sa",
+ "login_disabled": "Prihlásenie je zakázané",
+ "action_bar_logo_alt": "ntfy logo",
+ "action_bar_settings": "Nastavenia",
+ "action_bar_account": "Účet",
+ "action_bar_sign_in": "Prihlásiť sa",
+ "action_bar_profile_settings": "Nastavenia",
+ "action_bar_reservation_edit": "Zmeniť rezerváciu",
+ "action_bar_unsubscribe": "Odhlásiť odber",
+ "action_bar_toggle_mute": "Stlmiť/zrušiť stlmenie upozornení",
+ "action_bar_toggle_action_menu": "Otvoriť/zavrieť akčné menu",
+ "action_bar_profile_title": "Profil",
+ "nav_button_settings": "Nastavenia",
+ "nav_button_account": "Účet",
+ "message_bar_show_dialog": "Zobraziť okno pre odosielanie oznámení",
+ "message_bar_publish": "Zverejniť správu",
+ "nav_topics_title": "Odoberané témy",
+ "nav_button_all_notifications": "Všetky oznámenia",
+ "alert_grant_description": "Udeliť prehliadaču povolenie na zobrazovanie oznámení na ploche.",
+ "alert_not_supported_context_description": "Oznámenia sú podporované len cez HTTPS. Ide o obmedzenie rozhrania Notifications API.",
+ "notifications_list": "Zoznam oznámení",
+ "notifications_list_item": "Oznámenie",
+ "notifications_mark_read": "Označiť ako prečítané",
+ "notifications_delete": "Zmazať",
+ "notifications_copied_to_clipboard": "Skopírované do schránky",
+ "notifications_tags": "Štítky",
+ "notifications_priority_x": "Priorita {{priority}}",
+ "notifications_new_indicator": "Nové oznámenie",
+ "notifications_attachment_image": "Obrázok prílohy",
+ "notifications_attachment_link_expired": "odkaz na stiahnutie vypršal",
+ "notifications_attachment_file_image": "súbor s obrázkom",
+ "notifications_attachment_file_video": "video súbor",
+ "notifications_attachment_file_audio": "zvukový súbor",
+ "notifications_attachment_file_app": "Súbor aplikácie pre Android",
+ "notifications_attachment_file_document": "iný dokument",
+ "notifications_click_copy_url_title": "Skopírovať URL adresu odkazu do schránky",
+ "notifications_click_copy_url_button": "Kopírovať odkaz",
+ "notifications_click_open_button": "Otvoriť odkaz",
+ "notifications_actions_not_supported": "Akcia nie je podporovaná vo webovej aplikácii",
+ "notifications_none_for_topic_title": "K tejto téme ste zatiaľ nedostali žiadne upozornenia.",
+ "notifications_none_for_any_title": "Nedostali ste žiadne upozornenia.",
+ "notifications_none_for_any_description": "Ak chcete posielať oznámenia do témy, jednoducho zadajte adresu PUT alebo POST na adresu URL témy. Tu je príklad s použitím jednej z vašich tém.",
+ "notifications_no_subscriptions_title": "Zdá sa, že zatiaľ nemáte žiadne prihlásenia na odber.",
+ "display_name_dialog_title": "Zmeniť zobrazovaný názov",
+ "notifications_no_subscriptions_description": "Kliknutím na odkaz \"{{text odkazu}}\" vytvoríte tému alebo sa na ňu prihlásite. Potom môžete posielať správy prostredníctvom PUT alebo POST a budete tu dostávať oznámenia.",
+ "notifications_example": "Príklad",
+ "notifications_more_details": "Ďalšie informácie nájdete na webovej stránke alebo v dokumentácií.",
+ "display_name_dialog_placeholder": "Zobrazený názov",
+ "reserve_dialog_checkbox_label": "Rezervovať tému a nakonfigurovať prístup",
+ "notifications_loading": "Načítavanie oznámení …",
+ "publish_dialog_title_no_topic": "Zverejniť oznámenie",
+ "publish_dialog_title_topic": "Zverejniť v {{topic}}",
+ "publish_dialog_progress_uploading": "Nahrávanie…",
+ "publish_dialog_progress_uploading_detail": "Nahrávanie {{loaded}}/{{total}} ({{percent}}%) …",
+ "publish_dialog_message_published": "Oznámenie zverejnené",
+ "publish_dialog_attachment_limits_file_and_quota_reached": "prekročí {{fileSizeLimit}} limit súboru a kvótu, {{remainingBytes}} zostáva",
+ "publish_dialog_attachment_limits_file_reached": "prekračuje {{fileSizeLimit}} limit súboru",
+ "publish_dialog_attachment_limits_quota_reached": "prekračuje kvótu, {{remainingBytes}} zostáva",
+ "publish_dialog_emoji_picker_show": "Vyberte emoji",
+ "publish_dialog_priority_min": "Min. priorita",
+ "publish_dialog_priority_low": "Nízka priorita",
+ "publish_dialog_priority_default": "Predvolená priorita",
+ "publish_dialog_priority_high": "Vysoká priorita",
+ "publish_dialog_priority_max": "Max. priorita",
+ "publish_dialog_base_url_label": "URL Adresa služby",
+ "publish_dialog_base_url_placeholder": "URL adresa služby, napr. https://example.com",
+ "publish_dialog_topic_label": "Názov témy",
+ "publish_dialog_topic_placeholder": "Názov témy, napr. phil_alerts",
+ "publish_dialog_topic_reset": "Resetovať tému",
+ "publish_dialog_title_label": "Názov",
+ "publish_dialog_title_placeholder": "Názov oznámenia, napr. Upozornenie na miesto na disku",
+ "publish_dialog_tags_label": "Štítky",
+ "publish_dialog_message_label": "Správa",
+ "publish_dialog_priority_label": "Priorita",
+ "publish_dialog_click_label": "Kliknite na URL",
+ "publish_dialog_click_placeholder": "URL adresa sa otvorí po kliknutí na oznámenie",
+ "publish_dialog_email_label": "Email",
+ "publish_dialog_email_placeholder": "Emailová adresa, na ktorú sa má oznámenie zaslať, napr. phil@example.com",
+ "publish_dialog_call_label": "Telefonovať",
+ "publish_dialog_call_item": "Zavolať na telefónne číslo {{number}}",
+ "publish_dialog_call_reset": "Odstrániť telefón",
+ "publish_dialog_attach_label": "URL prílohy",
+ "publish_dialog_attach_reset": "Odstrániť URL prílohy",
+ "publish_dialog_filename_label": "Názov súboru",
+ "publish_dialog_filename_placeholder": "Názov súboru prílohy",
+ "publish_dialog_delay_label": "Oneskorenie",
+ "publish_dialog_delay_placeholder": "Oneskorenie doručenia, napr. {{unixTimestamp}}, {{relativeTime}} alebo \"{{naturalLanguage}}\" (len v angličtine)",
+ "publish_dialog_delay_reset": "Odstrániť oneskorené doručenie",
+ "publish_dialog_chip_call_label": "Telefonovať",
+ "publish_dialog_other_features": "Ďalšie funkcie:",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Žiadne overené telefónne čísla",
+ "publish_dialog_chip_attach_url_label": "Pripojiť súbor pomocou adresy URL",
+ "publish_dialog_chip_delay_label": "Oneskoriť doručenie",
+ "publish_dialog_chip_topic_label": "Zmeniť tému",
+ "publish_dialog_button_cancel_sending": "Zrušiť odosielanie",
+ "publish_dialog_button_send": "Odoslať",
+ "publish_dialog_checkbox_publish_another": "Zverejniť ďalšie",
+ "publish_dialog_attached_file_title": "Priložený súbor:",
+ "subscribe_dialog_subscribe_button_cancel": "Zrušiť",
+ "subscribe_dialog_subscribe_title": "Odoberať tému",
+ "subscribe_dialog_subscribe_base_url_label": "URL Adresa služby",
+ "subscribe_dialog_subscribe_topic_placeholder": "Názov témy, napr. phil_alerts",
+ "publish_dialog_attached_file_filename_placeholder": "Názov súboru prílohy",
+ "publish_dialog_attached_file_remove": "Odstrániť priložený súbor",
+ "publish_dialog_drop_file_here": "Vložiť súbor",
+ "subscribe_dialog_login_password_label": "Heslo",
+ "account_basics_password_dialog_confirm_password_label": "Potvrdenie hesla",
+ "account_basics_title": "Účet",
+ "account_delete_dialog_button_cancel": "Zrušiť",
+ "account_delete_dialog_label": "Heslo",
+ "prefs_reservations_dialog_title_add": "Rezervovať tému",
+ "publish_dialog_button_cancel": "Zrušiť",
+ "account_upgrade_dialog_button_cancel": "Zrušiť",
+ "account_tokens_dialog_button_cancel": "Zrušiť",
+ "common_cancel": "Zrušiť",
+ "common_add": "Pridať",
+ "account_basics_username_title": "Používateľské meno",
+ "signup_form_password": "Heslo",
+ "signup_error_creation_limit_reached": "Dosiahnutý limit na vytvorenie konta",
+ "account_basics_password_title": "Heslo",
+ "action_bar_change_display_name": "Zmeniť zobrazovaný názov",
+ "prefs_users_dialog_password_label": "Heslo",
+ "action_bar_sign_up": "Zaregistrovať sa",
+ "login_link_signup": "Zaregistrovať sa",
+ "signup_already_have_account": "Už máte účet? Prihláste sa!",
+ "signup_disabled": "Registrácia je vypnutá",
+ "login_title": "Prihláste sa do svojho konta ntfy",
+ "action_bar_show_menu": "Zobraziť menu",
+ "action_bar_reservation_add": "Rezervovať tému",
+ "action_bar_reservation_delete": "Odstrániť rezerváciu",
+ "action_bar_reservation_limit_reached": "Dosiahnutý limit",
+ "action_bar_send_test_notification": "Odoslať testovacie oznámenie",
+ "action_bar_clear_notifications": "Vymazať všetky oznámenia",
+ "publish_dialog_message_placeholder": "Sem napíšte správu",
+ "action_bar_profile_logout": "Odhlásiť sa",
+ "message_bar_type_message": "Sem napíšte správu",
+ "message_bar_error_publishing": "Chyba pri zverejňovaní oznámenia",
+ "nav_button_documentation": "Dokumentácia",
+ "nav_button_publish_message": "Zverejniť oznámenie",
+ "nav_button_subscribe": "Odoberať tému",
+ "nav_button_muted": "Oznámenia stlmené",
+ "nav_button_connecting": "pripájanie",
+ "nav_upgrade_banner_description": "Rezervovať témy, viac správ a e-mailov a väčšie prílohy",
+ "nav_upgrade_banner_label": "Vylepšiť na ntfy Pro",
+ "alert_grant_title": "Oznámenia sú vypnuté",
+ "alert_grant_button": "Prideliť teraz",
+ "alert_not_supported_title": "Oznámenia nie sú podporované",
+ "alert_not_supported_description": "Oznámenia nie sú vo vašom prehliadači podporované.",
+ "notifications_attachment_copy_url_title": "Kopírovať URL adresu prílohy do schránky",
+ "notifications_attachment_copy_url_button": "Kopírovať adresu URL",
+ "notifications_attachment_open_title": "Prejsť na {{url}}",
+ "notifications_actions_open_url_title": "Prejsť na {{url}}",
+ "notifications_attachment_open_button": "Otvoriť prílohu",
+ "notifications_attachment_link_expires": "platnosť odkazu vyprší {{date}}",
+ "notifications_none_for_topic_description": "Ak chcete posielať oznámenia do tejto témy, jednoducho zadajte adresu PUT alebo POST na URL adresu témy.",
+ "notifications_actions_http_request_title": "Odoslať HTTP {{method}} na {{url}}",
+ "display_name_dialog_description": "Nastavenie alternatívneho názvu témy, ktorá sa zobrazuje v zozname odberov. Pomáha to ľahšie identifikovať témy so zložitými názvami.",
+ "prefs_users_table_base_url_header": "URL Adresa služby",
+ "publish_dialog_tags_placeholder": "Zoznam štítkov oddelených čiarkou, napr. varovanie, srv1-backup",
+ "publish_dialog_chip_click_label": "Kliknite na URL",
+ "publish_dialog_email_reset": "Odstrániť email na preposielanie",
+ "publish_dialog_click_reset": "Odobrať URL kliknutím",
+ "publish_dialog_attach_placeholder": "Pripojiť súbor pomocou URL adresy, napr. https://f-droid.org/F-Droid.apk",
+ "publish_dialog_chip_email_label": "Preposlanie na email",
+ "publish_dialog_chip_attach_file_label": "Pripojiť miestny súbor",
+ "publish_dialog_details_examples_description": "Príklady a podrobný opis všetkých funkcií odosielania nájdete v dokumentácii.",
+ "account_upgrade_dialog_tier_features_no_calls": "Žiadne telefonáty",
+ "account_upgrade_dialog_billing_contact_email": "V prípade otázok týkajúcich sa fakturácie nás prosím kontaktujte tu.",
+ "account_tokens_dialog_title_create": "Vytvoriť prístupový token",
+ "prefs_reservations_dialog_title_edit": "Upraviť rezervovanú tému",
+ "account_basics_tier_interval_monthly": "mesačne",
+ "account_basics_tier_canceled_subscription": "Vaše predplatné bolo zrušené a bude preradené na bezplatné konto k dátumu {{date}}.",
+ "priority_default": "predvolená",
+ "prefs_notifications_min_priority_title": "Najnižšia priorita",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} denný telefonát",
+ "account_upgrade_dialog_tier_current_label": "Aktuálne",
+ "account_basics_password_dialog_current_password_incorrect": "Nesprávne heslo",
+ "account_tokens_table_token_header": "Token",
+ "prefs_notifications_delete_after_never": "Nikdy",
+ "prefs_users_description": "Tu môžete pridávať/odstraňovať používateľov pre svoje chránené témy. Upozorňujeme, že používateľské meno a heslo sú uložené v lokálnom úložisku prehliadača.",
+ "account_basics_phone_numbers_dialog_number_label": "Telefónne číslo",
+ "subscribe_dialog_subscribe_description": "Témy nemusia byť chránené heslom, preto vyberte názov, ktorý nie je ľahké uhádnuť. Po prihlásení sa na odber môžete PUT/POST oznámenia.",
+ "account_basics_password_dialog_button_submit": "Zmeniť heslo",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Potvrdiť kód",
+ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "ušetrite až {{discount}}%",
+ "account_tokens_dialog_label": "Označenie, napr. Radarr notifications",
+ "account_tokens_table_expires_header": "Vyprší",
+ "account_upgrade_dialog_proration_info": "Vyhlásenie: Pri prechode medzi platenými plánmi sa rozdiel v cene účtuje okamžite. Pri prechode na nižšiu úroveň sa zostatok použije na platbu za budúce fakturačné obdobia.",
+ "prefs_reservations_dialog_access_label": "Prístup",
+ "account_usage_attachment_storage_title": "Ukladanie príloh",
+ "prefs_users_dialog_username_label": "Používateľské meno, napr. phil",
+ "account_usage_messages_title": "Zverejnené správy",
+ "emoji_picker_search_clear": "Vymazať vyhľadávanie",
+ "prefs_reservations_table_not_subscribed": "Odber nie je prihlásený",
+ "account_upgrade_dialog_tier_features_emails_other": "{{emails}} denné emaily",
+ "prefs_notifications_min_priority_max_only": "Iba najvyššia priorita",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} denné telefonáty",
+ "prefs_notifications_sound_description_some": "Oznámenia pri príchode prehrávajú zvuk {{sound}}",
+ "prefs_reservations_edit_button": "Upraviť prístup k téme",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Poslať SMS",
+ "account_basics_tier_change_button": "Zmeniť",
+ "account_tokens_dialog_expires_never": "Platnosť tokenu nikdy nevyprší",
+ "subscribe_dialog_login_title": "Vyžaduje sa prihlásenie",
+ "account_tokens_dialog_expires_x_days": "Token vyprší za {{days}} dní",
+ "prefs_reservations_table_everyone_read_only": "Môžem publikovať a odoberať, každý môže odoberať",
+ "prefs_reservations_table_everyone_deny_all": "Iba ja môžem publikovať a odoberať",
+ "account_basics_phone_numbers_dialog_description": "Ak chcete používať funkciu oznamovanie hovorom, musíte pridať a overiť aspoň jedno telefónne číslo. Overenie je možné vykonať prostredníctvom SMS alebo telefonického hovoru.",
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervovaná téma",
+ "account_delete_title": "Odstrániť účet",
+ "subscribe_dialog_login_button_login": "Prihlásenie",
+ "account_upgrade_dialog_tier_features_no_reservations": "Žiadne rezervované témy",
+ "prefs_users_table_cannot_delete_or_edit": "Nie je možné odstrániť alebo upraviť prihláseného používateľa",
+ "account_basics_tier_admin_suffix_with_tier": "(s úrovňou {{tier}})",
+ "prefs_notifications_delete_after_three_hours_description": "Oznámenia sa automaticky odstránia po troch hodinách",
+ "prefs_notifications_delete_after_three_hours": "Po troch hodinách",
+ "prefs_notifications_min_priority_description_x_or_higher": "Zobraziť oznámenia, ak je priorita {{number}} ({{name}}) alebo vyššia",
+ "reservation_delete_dialog_description": "Odstránením rezervácie sa vzdáte vlastníctva témy a umožníte ostatným, aby si ju rezervovali. Existujúce správy a prílohy si môžete ponechať alebo odstrániť.",
+ "subscribe_dialog_login_username_label": "Používateľské meno, napr. phil",
+ "subscribe_dialog_error_user_not_authorized": "Používateľ {{username}} nie je autorizovaný",
+ "prefs_reservations_table_everyone_read_write": "Každý môže publikovať a odoberať",
+ "prefs_reservations_dialog_title_delete": "Odstrániť rezervovanú tému",
+ "prefs_users_table": "Tabuľka používateľov",
+ "prefs_reservations_table_topic_header": "Téma",
+ "reservation_delete_dialog_submit_button": "Vymazať rezerváciu",
+ "prefs_reservations_limit_reached": "Dosiahli ste limit rezervovaných tém.",
+ "account_upgrade_dialog_interval_monthly": "Mesačne",
+ "prefs_users_add_button": "Pridať používateľa",
+ "account_upgrade_dialog_tier_features_messages_other": "{{messages}} denné správy",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Zatiaľ žiadne telefónne čísla",
+ "subscribe_dialog_subscribe_button_generate_topic_name": "Vygenerovať názov",
+ "prefs_appearance_language_title": "Jazyk",
+ "prefs_notifications_delete_after_one_day_description": "Oznámenia sa automaticky odstránia po jednom dni",
+ "subscribe_dialog_subscribe_button_subscribe": "Odoberať",
+ "account_tokens_table_never_expires": "Nikdy nevyprší",
+ "account_tokens_delete_dialog_title": "Odstrániť prístupový token",
+ "prefs_notifications_delete_after_one_month": "Po jednom mesiaci",
+ "account_basics_phone_numbers_dialog_title": "Pridať telefónne číslo",
+ "account_tokens_delete_dialog_description": "Pred odstránením prístupového tokenu sa uistite, že ho aktívne nepoužívajú žiadne aplikácie ani skripty. Túto akciu nie je možné vrátiť späť.",
+ "account_tokens_table_label_header": "Označenie",
+ "account_upgrade_dialog_billing_contact_website": "Otázky týkajúce sa fakturácie nájdete na našej webovej stránke.",
+ "account_basics_username_admin_tooltip": "Ste Admin",
+ "prefs_notifications_delete_after_never_description": "Oznámenia sa nikdy automaticky neodstránia",
+ "account_delete_dialog_description": "Tým sa vaše konto natrvalo odstráni vrátane všetkých údajov uložených na serveri. Po vymazaní bude vaše používateľské meno 7 dní nedostupné. Ak naozaj chcete pokračovať, potvrďte svoje heslo v poli nižšie.",
+ "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} rezervované témy",
+ "account_usage_reservations_none": "Žiadne rezervované témy pre toto konto",
+ "prefs_notifications_sound_description_none": "Pri príchode oznámení sa neprehráva žiadny zvuk",
+ "account_tokens_description": "Pri publikovaní a prihlasovaní prostredníctvom rozhrania ntfy API používajte prístupové tokeny, aby ste nemuseli posielať prihlasovacie údaje k účtu. Viacej informácií nájdete v dokumentácií.",
+ "prefs_reservations_table": "Tabuľka rezervovaných tém",
+ "emoji_picker_search_placeholder": "Vyhľadať emoji",
+ "account_upgrade_dialog_button_cancel_subscription": "Zrušiť predplatné",
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} denný email",
+ "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} na jeden súbor",
+ "prefs_reservations_description": "Tu si môžete rezervovať názvy tém na osobné použitie. Rezervovaním témy získate vlastníctvo nad témou a môžete definovať prístupové práva pre ostatných používateľov k téme.",
+ "account_usage_title": "Používanie",
+ "account_basics_tier_upgrade_button": "Vylepšiť na PRO verziu",
+ "prefs_users_description_no_sync": "Používatelia a heslá nie sú synchronizované s vaším účtom.",
+ "account_tokens_dialog_title_edit": "Upraviť prístupový token",
+ "account_upgrade_dialog_tier_features_messages_one": "{{messages}} denná správa",
+ "account_upgrade_dialog_reservations_warning_one": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne vymažte aspoň jednu rezerváciu. Rezervácie môžete odstrániť v Nastaveniach.",
+ "subscribe_dialog_error_topic_already_reserved": "Téma je už rezervovaná",
+ "prefs_users_table_user_header": "Používateľ",
+ "error_boundary_stack_trace": "Výpis zásobníka",
+ "prefs_notifications_delete_after_one_week": "Po jednom týždni",
+ "prefs_reservations_delete_button": "Resetovať prístup k téme",
+ "account_basics_tier_admin_suffix_no_tier": "(bez úrovne)",
+ "prefs_notifications_delete_after_one_week_description": "Oznámenia sa automaticky odstránia po jednom týždni",
+ "error_boundary_unsupported_indexeddb_description": "Webová aplikácia ntfy potrebuje na fungovanie IndexedDB a váš prehliadač nepodporuje IndexedDB v režime súkromného prehliadania.
Je to síce nešťastné, ale aj tak nemá veľký zmysel používať webovú aplikáciu ntfy v režime súkromného prehliadania, pretože všetko je uložené v úložisku prehliadača. Viac informácií si môžete prečítať v tomto probléme GitHubu alebo sa s nami porozprávať na Discord alebo Matrix.",
+ "account_basics_tier_payment_overdue": "Vaša platba je po termíne splatnosti. Aktualizujte prosím svoj spôsob platby, inak bude váš účet preradený do nižšej kategórie.",
+ "account_basics_tier_description": "Úroveň výkonu vášho účtu",
+ "account_basics_phone_numbers_description": "Pre oznamovanie hovorom",
+ "account_basics_tier_free": "Zadarmo",
+ "account_upgrade_dialog_cancel_warning": "Týmto zrušíte svoje predplatné a {{date}} prejdete na nižšiu úroveň svojho účtu. V tento deň budú odstránené rezervácie tém, ako aj správy uložené vo vyrovnávacej pamäti servera.",
+ "account_basics_tier_admin": "Admin",
+ "prefs_notifications_sound_title": "Zvuk oznámenia",
+ "prefs_notifications_min_priority_default_and_higher": "Predvolená priorita a vyššia",
+ "prefs_reservations_table_access_header": "Prístup",
+ "account_tokens_table_copied_to_clipboard": "Prístupový token skopírovaný",
+ "account_tokens_dialog_expires_x_hours": "Token vyprší za {{hours}} hodín",
+ "prefs_users_edit_button": "Upraviť používateľa",
+ "account_upgrade_dialog_title": "Zmeniť úroveň účtu",
+ "priority_low": "nízka",
+ "prefs_reservations_table_click_to_subscribe": "Kliknutím sa prihlásite na odber",
+ "account_basics_password_description": "Zmeniť heslo účtu",
+ "account_usage_calls_title": "Uskutočnené telefonické hovory",
+ "error_boundary_description": "Toto samozrejme nemalo nastať. Je mi to veľmi ľúto. Ak máte chvíľu, nahláste to na GitHub alebo nám dajte vedieť cez Discord alebo Matrix.",
+ "priority_min": "najnižšia",
+ "account_basics_tier_basic": "Základný",
+ "prefs_notifications_min_priority_description_any": "Zobraziť všetky oznámenia bez ohľadu na prioritu",
+ "error_boundary_gathering_info": "Získajte viac informácií…",
+ "error_boundary_unsupported_indexeddb_title": "Súkromné prehliadanie nie je podporované",
+ "prefs_notifications_delete_after_one_day": "Po jednom dni",
+ "error_boundary_title": "Ale nie, ntfy prestalo fungovať",
+ "reservation_delete_dialog_action_keep_description": "Správy a prílohy, ktoré sú uložené v medzipamäti na serveri, budú verejne viditeľné pre ľudí, ktorí poznajú názov témy.",
+ "prefs_reservations_add_button": "Pridať rezervovanú tému",
+ "prefs_reservations_title": "Rezervované témy",
+ "account_basics_phone_numbers_copied_to_clipboard": "Telefónne číslo skopírované do schránky",
+ "prefs_reservations_dialog_description": "Rezervovaním témy získate vlastníctvo nad témou a môžete definovať prístupové práva pre ostatných používateľov k téme.",
+ "account_basics_tier_title": "Typ účtu",
+ "account_usage_cannot_create_portal_session": "Nemožnosť otvoriť fakturačný portál",
+ "account_tokens_delete_dialog_submit_button": "Trvalo odstrániť token",
+ "account_delete_description": "Natrvalo odstrániť vaše konto",
+ "account_basics_phone_numbers_dialog_number_placeholder": "napr. +1222333444",
+ "account_basics_phone_numbers_dialog_code_placeholder": "napr. 123456",
+ "prefs_notifications_title": "Oznámenia",
+ "account_basics_tier_manage_billing_button": "Spravovať fakturáciu",
+ "account_tokens_title": "Prístupové tokeny",
+ "account_basics_username_description": "Hej, to si ty ❤",
+ "prefs_reservations_dialog_topic_label": "Téma",
+ "prefs_users_title": "Správa používateľov",
+ "account_basics_tier_interval_yearly": "ročne",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje sa mesačne.",
+ "account_delete_dialog_button_submit": "Natrvalo odstrániť konto",
+ "account_basics_phone_numbers_dialog_channel_call": "Hovor",
+ "account_basics_password_dialog_new_password_label": "Nové heslo",
+ "account_tokens_dialog_expires_unchanged": "Ponechať dátum skončenia platnosti nezmenený",
+ "error_boundary_button_copy_stack_trace": "Kopírovať výpis zásobníka",
+ "account_tokens_dialog_title_delete": "Odstrániť prístupový token",
+ "account_usage_of_limit": "z {{limit}}",
+ "reservation_delete_dialog_action_keep_title": "Ponechať správy a prílohy uložené v medzipamäti",
+ "prefs_notifications_sound_no_sound": "Bez zvuku",
+ "account_upgrade_dialog_interval_yearly": "Ročne",
+ "account_upgrade_dialog_button_redirect_signup": "Zaregistrujte sa teraz",
+ "subscribe_dialog_error_user_anonymous": "anonymný",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} účtovaná ročne. Uložiť {{save}}.",
+ "prefs_notifications_min_priority_high_and_higher": "Vysoká priorita a vyššia",
+ "account_usage_basis_ip_description": "Štatistiky a limity používania tohto účtu sú založené na vašej IP adrese, takže môžu byť zdieľané s ostatnými používateľmi. Vyššie uvedené limity sú približné hodnoty založené na existujúcich rýchlostných limitoch.",
+ "account_basics_password_dialog_title": "Zmeniť heslo",
+ "priority_max": "najvyššia",
+ "account_usage_limits_reset_daily": "Limity používania sa obnovujú denne o polnoci (UTC)",
+ "account_usage_unlimited": "Nekonečné",
+ "prefs_users_delete_button": "Odstrániť používateľa",
+ "prefs_notifications_min_priority_any": "Akákoľvek priorita",
+ "account_tokens_dialog_expires_label": "Platnosť prístupového tokenu vyprší za",
+ "account_basics_phone_numbers_title": "Telefónne čísla",
+ "prefs_notifications_delete_after_title": "Odstrániť oznámenia",
+ "account_upgrade_dialog_interval_yearly_discount_save": "ušetríte {{discount}}%",
+ "prefs_users_dialog_title_edit": "Upraviť používateľa",
+ "account_basics_password_dialog_current_password_label": "Aktuálne heslo",
+ "prefs_notifications_min_priority_low_and_higher": "Nízka priorita a vyššia",
+ "account_tokens_dialog_button_update": "Aktualizovať token",
+ "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} celkový úložný priestor",
+ "prefs_reservations_table_everyone_write_only": "Môžem publikovať a odoberať, každý môže publikovať",
+ "prefs_appearance_title": "Vzhlad",
+ "account_tokens_table_cannot_delete_or_edit": "Nie je možné upraviť alebo odstrániť aktuálny token relácie",
+ "prefs_notifications_sound_play": "Prehrať vybraný zvuk",
+ "account_tokens_table_last_access_header": "Posledný prístup",
+ "account_tokens_table_last_origin_tooltip": "Z IP adresy {{ip}}, kliknite na vyhľadávanie",
+ "account_usage_reservations_title": "Rezervované témy",
+ "account_upgrade_dialog_tier_price_per_month": "mesiac",
+ "account_usage_calls_none": "S týmto účtom nie je možné uskutočňovať žiadne telefonické hovory",
+ "account_tokens_table_current_session": "Aktuálna relácia prehliadača",
+ "account_upgrade_dialog_button_pay_now": "Zaplatiť a predplatiť si",
+ "subscribe_dialog_subscribe_use_another_label": "Použiť iný server",
+ "reservation_delete_dialog_action_delete_title": "Odstrániť správy a prílohy uložené v medzipamäti",
+ "account_basics_phone_numbers_dialog_code_label": "Overovací kód",
+ "reservation_delete_dialog_action_delete_description": "Správy a prílohy uložené v medzipamäti sa natrvalo vymažú. Túto akciu nemožno vrátiť späť.",
+ "account_basics_tier_paid_until": "Predplatné zaplatené do {{date}} s automatickou obnovou",
+ "account_usage_attachment_storage_description": "{{filesize}} na súbor, vymazaný po {{expiry}}",
+ "prefs_notifications_delete_after_one_month_description": "Oznámenia sa automaticky odstránia po jednom mesiaci",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Zavolajte mi",
+ "prefs_users_dialog_base_url_label": "URL adresa služby, napr. https://ntfy.sh",
+ "account_usage_emails_title": "Odoslané emaily",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_upgrade_dialog_tier_selected_label": "Vybrané",
+ "account_upgrade_dialog_button_update_subscription": "Aktualizovať predplatné",
+ "priority_high": "vysoká",
+ "account_delete_dialog_billing_warning": "Odstránením konta sa okamžite zruší aj vaše fakturačné predplatné. Už nebudete mať prístup k fakturačnému panelu.",
+ "prefs_notifications_min_priority_description_max": "Zobraziť oznámenia, ak je priorita 5 (max)",
+ "subscribe_dialog_login_description": "Táto téma je chránená heslom. Ak sa chcete prihlásiť na odber témy, zadajte používateľské meno a heslo.",
+ "account_upgrade_dialog_reservations_warning_other": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne vymažte aspoň {{count}} rezervácií. Rezervácie môžete odstrániť v Nastaveniach.",
+ "prefs_users_dialog_title_add": "Pridať používateľa",
+ "account_tokens_dialog_button_create": "Vytvoriť token",
+ "account_tokens_table_create_token_button": "Vytvoriť prístupový token"
+}
diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json
index 2cebdd1c..1a44a3dc 100644
--- a/web/public/static/langs/sv.json
+++ b/web/public/static/langs/sv.json
@@ -277,7 +277,7 @@
"publish_dialog_priority_low": "Låg prioritet",
"publish_dialog_priority_default": "Standard prioritet",
"publish_dialog_priority_high": "Hög prioritet",
- "publish_dialog_priority_max": "Högsta prioritet",
+ "publish_dialog_priority_max": "Max. prioritet",
"publish_dialog_base_url_label": "Service-URL",
"publish_dialog_email_label": "E-post",
"publish_dialog_attach_reset": "Ta bort URL för bifogade filer",
diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json
index be8ddf47..28eca9f6 100644
--- a/web/public/static/langs/tr.json
+++ b/web/public/static/langs/tr.json
@@ -77,7 +77,7 @@
"notifications_example": "Örnek",
"notifications_more_details": "Daha fazla bilgi için web sitesine veya belgelendirmeye bakın.",
"publish_dialog_chip_attach_url_label": "URL ile dosya ekle",
- "prefs_notifications_min_priority_default_and_higher": "Öntanımlı öncelik ve üstü",
+ "prefs_notifications_min_priority_default_and_higher": "Varsayılan öncelik ve üstü",
"prefs_notifications_delete_after_three_hours": "Üç saat sonra",
"notifications_none_for_any_description": "Bir konuya bildirim göndermek için konu URL'sine PUT veya POST göndermeniz yeterlidir. İşte konularınızdan birini kullanan bir örnek.",
"notifications_no_subscriptions_title": "Henüz aboneliğiniz yok gibi görünüyor.",
diff --git a/web/public/static/langs/uz.json b/web/public/static/langs/uz.json
new file mode 100644
index 00000000..b951406a
--- /dev/null
+++ b/web/public/static/langs/uz.json
@@ -0,0 +1,27 @@
+{
+ "signup_title": "ntfy hisobini yaratish",
+ "signup_form_password": "Parol",
+ "signup_form_confirm_password": "Parolni tasdiqlang",
+ "signup_error_username_taken": "Foydalanuvchi nomi {{username}} allaqachon foydalanilmoqda",
+ "signup_error_creation_limit_reached": "Boshqa hisob raqam ocha olmaysiz",
+ "login_title": "Ntfy hisobingizga kiring",
+ "login_form_button_submit": "Kirish",
+ "login_link_signup": "Ro'yxatdan o'tish",
+ "login_disabled": "Kirish o'chirilgan",
+ "action_bar_show_menu": "Menyuni ko'rsatish",
+ "action_bar_logo_alt": "ntfy logotipi",
+ "action_bar_settings": "Sozlamalar",
+ "action_bar_change_display_name": "Ko'rsatilgan nomni o'zgartiring",
+ "action_bar_reservation_add": "Zaxira mavzusi",
+ "common_cancel": "Bekor qilish",
+ "common_save": "Saqlash",
+ "common_add": "Qo‘shish",
+ "common_back": "Orqaga",
+ "common_copy_to_clipboard": "Xotiraga nusxalash",
+ "signup_form_username": "Foydalanuvchi nomi",
+ "signup_form_button_submit": "Ro‘yxatdan o‘tish",
+ "signup_form_toggle_password_visibility": "Parol ko‘rinishini o‘zgartirish",
+ "signup_already_have_account": "Hisobingiz bormi? Tizimga kiring!",
+ "signup_disabled": "Ro‘yxatdan o‘tish o‘chirilgan",
+ "action_bar_account": "Hisob"
+}
diff --git a/web/public/static/langs/vi.json b/web/public/static/langs/vi.json
new file mode 100644
index 00000000..b2f94441
--- /dev/null
+++ b/web/public/static/langs/vi.json
@@ -0,0 +1,21 @@
+{
+ "common_add": "Thêm",
+ "common_back": "Quay lại",
+ "signup_title": "Tạo tài khoản ntfy",
+ "signup_form_toggle_password_visibility": "Hiện mật khẩu",
+ "login_form_button_submit": "Đăng nhập",
+ "common_copy_to_clipboard": "Lưu vào clipboard",
+ "signup_form_username": "Tên user",
+ "signup_already_have_account": "Đã có tài khoản? Đăng nhập!",
+ "signup_disabled": "Đăng kí bị đóng",
+ "signup_error_username_taken": "Tên {{username}} đã được sử dụng",
+ "signup_error_creation_limit_reached": "Đã bị giới hạn tạo tài khoản",
+ "login_title": "Đăng nhập vào tài khoản ntfy",
+ "login_link_signup": "Đăng kí",
+ "login_disabled": "Đăng nhập bị đóng",
+ "action_bar_show_menu": "Hiện menu",
+ "signup_form_password": "Mật khẩu",
+ "action_bar_settings": "Cài đặt",
+ "signup_form_confirm_password": "Xác nhận mật khẩu",
+ "signup_form_button_submit": "Đăng kí"
+}
diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json
index 60542465..e26e7f14 100644
--- a/web/public/static/langs/zh_Hans.json
+++ b/web/public/static/langs/zh_Hans.json
@@ -1,11 +1,13 @@
{
"action_bar_show_menu": "显示菜单",
"action_bar_logo_alt": "ntfy图标",
+ "action_bar_mute_notifications": "静音",
"action_bar_settings": "设置",
"action_bar_send_test_notification": "发送测试通知",
"action_bar_clear_notifications": "清除所有通知",
"action_bar_unsubscribe": "取消订阅",
"action_bar_toggle_action_menu": "开启或关闭操作菜单",
+ "action_bar_unmute_notifications": "取消静音",
"message_bar_type_message": "在此处输入消息",
"message_bar_show_dialog": "显示发布对话框",
"message_bar_publish": "发布消息",
@@ -20,6 +22,10 @@
"alert_notification_permission_required_button": "现在授予",
"alert_not_supported_title": "不支持通知",
"alert_not_supported_description": "您的浏览器不支持通知。",
+ "alert_notification_ios_install_required_description": "要接收通知,请在iOS上点击分享图标,然后添加到主屏幕。",
+ "alert_notification_ios_install_required_title": "需要安装iOS应用程序",
+ "alert_notification_permission_denied_description": "你已禁用通知。要重新启用通知,请在浏览器设置中启用通知。",
+ "alert_notification_permission_denied_title": "已禁用通知",
"notifications_list": "通知列表",
"notifications_list_item": "通知",
"notifications_mark_read": "标记为已读",
@@ -117,9 +123,9 @@
"prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知",
"prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知",
"prefs_notifications_min_priority_any": "任意优先级",
- "prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级",
- "prefs_notifications_min_priority_default_and_higher": "默认优先级和更高优先级",
- "prefs_notifications_min_priority_high_and_higher": "高优先级和更高优先级",
+ "prefs_notifications_min_priority_low_and_higher": "低优先级或更高",
+ "prefs_notifications_min_priority_default_and_higher": "默认优先级或更高",
+ "prefs_notifications_min_priority_high_and_higher": "高优先级或更高",
"prefs_notifications_min_priority_max_only": "仅最高优先级",
"prefs_notifications_delete_after_never": "从不",
"prefs_notifications_delete_after_one_month": "一月后",
@@ -129,6 +135,11 @@
"prefs_notifications_delete_after_one_day_description": "一天后自动删除通知",
"prefs_notifications_delete_after_one_week_description": "一周后自动删除通知",
"prefs_notifications_delete_after_one_month_description": "一月后后自动删除通知",
+ "prefs_notifications_web_push_disabled": "已暂用",
+ "prefs_notifications_web_push_disabled_description": "当网页程序在运行时将会收到通知 (透过 WebSocket)",
+ "prefs_notifications_web_push_enabled": "已为 {{server}} 启用",
+ "prefs_notifications_web_push_enabled_description": "即使网页程序未有运行亦会收到通知 (via Web Push)",
+ "prefs_notifications_web_push_title": "背景通知",
"prefs_users_title": "管理用户",
"prefs_users_description": "在此处添加/删除受保护主题的用户。请注意,用户名和密码存储在浏览器的本地存储中。",
"prefs_users_add_button": "添加用户",
@@ -140,6 +151,10 @@
"common_save": "保存",
"prefs_appearance_title": "外观",
"prefs_appearance_language_title": "语言",
+ "prefs_appearance_theme_title": "主題",
+ "prefs_appearance_theme_system": "系統 (預設)",
+ "prefs_appearance_theme_dark": "黑暗模式",
+ "prefs_appearance_theme_light": "光亮模式",
"priority_min": "最低",
"priority_low": "低",
"priority_default": "默认",
@@ -149,6 +164,7 @@
"prefs_users_table_base_url_header": "服务链接地址",
"prefs_users_dialog_base_url_label": "服务链接地址,例如 https://ntfy.sh",
"error_boundary_button_copy_stack_trace": "复制堆栈跟踪",
+ "error_boundary_button_reload_ntfy": "重新加载 ntfy",
"error_boundary_stack_trace": "堆栈跟踪",
"error_boundary_gathering_info": "收集更多信息……",
"error_boundary_unsupported_indexeddb_title": "不支持隐私浏览",
@@ -160,6 +176,7 @@
"notifications_attachment_copy_url_button": "复制链接地址",
"notifications_attachment_open_title": "转到 {{url}}",
"notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}",
+ "notifications_actions_failed_notification": "通知失败",
"notifications_actions_open_url_title": "转到 {{url}}",
"notifications_none_for_topic_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。",
"subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts",
@@ -168,12 +185,14 @@
"publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警",
"publish_dialog_email_label": "电子邮件",
"publish_dialog_button_send": "发送",
+ "publish_dialog_checkbox_markdown": "格式化为 Markdown",
"publish_dialog_attachment_limits_quota_reached": "超过配额,剩余 {{remainingBytes}}",
"publish_dialog_attach_label": "附件链接地址",
"publish_dialog_click_reset": "移除点击连接地址",
"publish_dialog_button_cancel": "取消",
"subscribe_dialog_subscribe_button_cancel": "取消",
"subscribe_dialog_subscribe_base_url_label": "服务地址地址",
+ "subscribe_dialog_subscribe_use_another_background_info": "当网页程序未开启, 将不会收到来自其他服务器的通知",
"prefs_notifications_min_priority_description_any": "显示所有通知,无论优先级如何",
"prefs_notifications_delete_after_title": "删除通知",
"prefs_notifications_delete_after_three_hours": "三小时后",
@@ -359,5 +378,30 @@
"publish_dialog_chip_call_no_verified_numbers_tooltip": "未验证的手机号",
"account_basics_phone_numbers_title": "电话号码",
"account_basics_phone_numbers_description": "电话通知",
- "account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。"
+ "account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。",
+ "account_basics_phone_numbers_dialog_code_label": "验证码",
+ "account_basics_phone_numbers_dialog_code_placeholder": "例如:123456",
+ "account_basics_phone_numbers_dialog_check_verification_button": "确认码",
+ "account_basics_phone_numbers_dialog_channel_sms": "短信",
+ "account_basics_phone_numbers_dialog_channel_call": "拨打",
+ "publish_dialog_call_reset": "清空拨号",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "无可执行的电话号码",
+ "account_basics_phone_numbers_dialog_title": "添加电话号码",
+ "account_basics_phone_numbers_copied_to_clipboard": "电话号码已复制到剪贴板",
+ "account_basics_phone_numbers_dialog_number_label": "电话号码",
+ "account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444",
+ "account_usage_calls_title": "已拨打电话",
+ "account_usage_calls_none": "此帐号无法拨打电话",
+ "account_upgrade_dialog_tier_features_reservations_one": "一条保留主题",
+ "account_upgrade_dialog_tier_features_emails_one": "一封每日邮件",
+ "account_upgrade_dialog_tier_features_calls_one": "一通每日电话",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "发送信息",
+ "account_basics_phone_numbers_dialog_verify_button_call": "拨打电话",
+ "account_upgrade_dialog_tier_features_messages_one": "一条每日消息",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} 通每日电话",
+ "account_upgrade_dialog_tier_features_no_calls": "无电话呼叫",
+ "web_push_subscription_expiring_title": "通知将被暂停",
+ "web_push_subscription_expiring_body": "打开ntfy以继续接收通知",
+ "web_push_unknown_notification_title": "接收到未知通知",
+ "web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy"
}
diff --git a/web/public/static/langs/zh_Hant.json b/web/public/static/langs/zh_Hant.json
index 9b0dd372..683f5a9f 100644
--- a/web/public/static/langs/zh_Hant.json
+++ b/web/public/static/langs/zh_Hant.json
@@ -1,220 +1,407 @@
{
- "action_bar_logo_alt": "ntfy 標識",
- "action_bar_unsubscribe": "取消訂閱",
- "action_bar_toggle_mute": "通知靜音/解除通知靜音",
- "action_bar_toggle_action_menu": "開啟/關閉操作選單",
- "message_bar_type_message": "在這輸入訊息",
- "alert_notification_permission_required_description": "允許瀏覽器權限以顯示桌面通知。",
- "alert_notification_permission_required_button": "允許",
- "notifications_list": "通知清單",
- "notifications_list_item": "通知",
- "notifications_mark_read": "標示已讀",
- "notifications_attachment_image": "附加圖片",
- "notifications_attachment_copy_url_title": "複製附件 URL 到剪貼簿",
- "notifications_attachment_copy_url_button": "複製 URL",
- "notifications_attachment_open_title": "前往 {{url}}",
- "notifications_attachment_open_button": "開啟附件",
- "notifications_attachment_link_expired": "下載連結已過期",
- "notifications_attachment_file_video": "影片檔案",
- "notifications_attachment_file_app": "Android 應用程式檔案",
- "notifications_attachment_file_document": "其他文件",
- "notifications_click_copy_url_title": "複製連結 URL 到剪貼板",
- "notifications_click_copy_url_button": "複製連結",
- "notifications_click_open_button": "開啟連結",
- "notifications_actions_not_supported": "網頁程式無法支援該動作",
- "notifications_actions_http_request_title": "傳送 HTTP {{method}} 到 {{url}}",
- "notifications_none_for_topic_title": "尚未收到任何此主題的通知。",
- "notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
- "notifications_none_for_any_title": "尚未收到任何通知。",
- "action_bar_settings": "設定",
- "action_bar_send_test_notification": "發送測試通知",
+ "account_basics_password_description": "更改你的帳戶密碼",
+ "account_basics_password_dialog_button_submit": "更改密碼",
+ "account_basics_password_dialog_confirm_password_label": "確認密碼",
+ "account_basics_password_dialog_current_password_incorrect": "密碼錯誤",
+ "account_basics_password_dialog_current_password_label": "當前密碼",
+ "account_basics_password_dialog_new_password_label": "新密碼",
+ "account_basics_password_dialog_title": "更改密碼",
+ "account_basics_password_title": "密碼",
+ "account_basics_phone_numbers_copied_to_clipboard": "電話號碼已複製到剪貼板",
+ "account_basics_phone_numbers_description": "電話通知",
+ "account_basics_phone_numbers_dialog_channel_call": "撥打",
+ "account_basics_phone_numbers_dialog_channel_sms": "短信",
+ "account_basics_phone_numbers_dialog_check_verification_button": "確認碼",
+ "account_basics_phone_numbers_dialog_code_label": "驗證碼",
+ "account_basics_phone_numbers_dialog_code_placeholder": "例如:123456",
+ "account_basics_phone_numbers_dialog_description": "要使用來電通知功能,你需要新增並驗證至少一個電話號碼。可以通過短信或電話驗證。",
+ "account_basics_phone_numbers_dialog_number_label": "電話號碼",
+ "account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444",
+ "account_basics_phone_numbers_dialog_title": "新增電話號碼",
+ "account_basics_phone_numbers_dialog_verify_button_call": "撥打電話",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "發送資訊",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "無可執行的電話號碼",
+ "account_basics_phone_numbers_title": "電話號碼",
+ "account_basics_tier_admin_suffix_no_tier": "(無等級)",
+ "account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等級)",
+ "account_basics_tier_admin": "管理員",
+ "account_basics_tier_basic": "基礎版",
+ "account_basics_tier_canceled_subscription": "你的訂閱已取消,並將在 {{date}} 降級為免費帳戶。",
+ "account_basics_tier_change_button": "改變",
+ "account_basics_tier_description": "你帳戶的權限級別",
+ "account_basics_tier_free": "免費",
+ "account_basics_tier_interval_monthly": "每月",
+ "account_basics_tier_interval_yearly": "每年",
+ "account_basics_tier_manage_billing_button": "管理計費",
+ "account_basics_tier_paid_until": "訂閱已支付至 {{date}},並將自動續訂",
+ "account_basics_tier_payment_overdue": "你的付款已逾期。請更新你的付款方式,否則你的帳戶將很快被降級。",
+ "account_basics_tier_title": "帳戶類型",
+ "account_basics_tier_upgrade_button": "升級到專業版",
+ "account_basics_title": "帳戶",
+ "account_basics_username_admin_tooltip": "你是管理員",
+ "account_basics_username_description": "嘿,那是你 ❤",
+ "account_basics_username_title": "用戶名",
+ "account_delete_description": "永久刪除你的帳戶",
+ "account_delete_dialog_billing_warning": "刪除你的帳戶也會立即取消你的計費訂閱。你將無法再訪問計費儀錶板。",
+ "account_delete_dialog_button_cancel": "取消",
+ "account_delete_dialog_button_submit": "永久刪除帳戶",
+ "account_delete_dialog_description": "這將永久刪除你的帳戶,包括存儲在伺服器上的所有數據。刪除後,你的用戶名將在 7 天內不可用。如果你真的想繼續,請在下面的框中使用你的密碼作確認。",
+ "account_delete_dialog_label": "密碼",
+ "account_delete_title": "刪除帳戶",
+ "account_tokens_delete_dialog_description": "在刪除訪問令牌之前,請確保沒有應用程序或腳本正在活躍使用它。 此操作無法撤銷。",
+ "account_tokens_delete_dialog_submit_button": "永久删除令牌",
+ "account_tokens_delete_dialog_title": "刪除訪問令牌",
+ "account_tokens_description": "通過 ntfy API 發布和訂閱時使用訪問令牌,因此你不必發送你的帳戶憑證。查看文檔以了解更多資訊。",
+ "account_tokens_dialog_button_cancel": "取消",
+ "account_tokens_dialog_button_create": "創建令牌",
+ "account_tokens_dialog_button_update": "更新令牌",
+ "account_tokens_dialog_expires_label": "訪問令牌過期於",
+ "account_tokens_dialog_expires_never": "令牌永不過期",
+ "account_tokens_dialog_expires_unchanged": "保持過期日期不變",
+ "account_tokens_dialog_expires_x_days": "令牌在 {{days}} 天後過期",
+ "account_tokens_dialog_expires_x_hours": "令牌在 {{hours}} 小時後過期",
+ "account_tokens_dialog_label": "標籤,例如:Radarr 通知",
+ "account_tokens_dialog_title_create": "創建訪問令牌",
+ "account_tokens_dialog_title_delete": "刪除訪問令牌",
+ "account_tokens_dialog_title_edit": "編輯訪問令牌",
+ "account_tokens_table_cannot_delete_or_edit": "無法編輯或刪除當前會話令牌",
+ "account_tokens_table_copied_to_clipboard": "已複製訪問令牌",
+ "account_tokens_table_create_token_button": "創建訪問令牌",
+ "account_tokens_table_current_session": "當前瀏覽器會話",
+ "account_tokens_table_expires_header": "過期",
+ "account_tokens_table_label_header": "標籤",
+ "account_tokens_table_last_access_header": "最後訪問",
+ "account_tokens_table_last_origin_tooltip": "於IP地址 {{ip}},點擊查找",
+ "account_tokens_table_never_expires": "永不過期",
+ "account_tokens_table_token_header": "令牌",
+ "account_tokens_title": "訪問令牌",
+ "account_upgrade_dialog_billing_contact_email": "有關賬單問題,請直接聯繫我們 。",
+ "account_upgrade_dialog_billing_contact_website": "有關賬單問題,請參考我們的網站 。",
+ "account_upgrade_dialog_button_cancel_subscription": "取消訂閱",
+ "account_upgrade_dialog_button_cancel": "取消",
+ "account_upgrade_dialog_button_pay_now": "立即付款並訂閱",
+ "account_upgrade_dialog_button_redirect_signup": "立即註冊",
+ "account_upgrade_dialog_button_update_subscription": "更新訂閱",
+ "account_upgrade_dialog_cancel_warning": "這將取消你的訂閱,並在 {{date}} 降級你的帳戶。在那一天,主題保留以及緩存在伺服器上的訊息將被刪除。",
+ "account_upgrade_dialog_interval_monthly": "每月",
+ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "節省高達 {{discount}}%",
+ "account_upgrade_dialog_interval_yearly_discount_save": "節省 {{discount}}%",
+ "account_upgrade_dialog_interval_yearly": "每年",
+ "account_upgrade_dialog_proration_info": "按比例分配:在付費計劃之間升級時,差價將被立刻收取。在降級到較低級別時,餘額將被用於支付未來的賬單周期。",
+ "account_upgrade_dialog_reservations_warning_one": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 1 項保留。你可以在設置中刪除保留。",
+ "account_upgrade_dialog_reservations_warning_other": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 {{count}} 項保留。你可以在設置中刪除保留。",
+ "account_upgrade_dialog_tier_current_label": "當前",
+ "account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}} ",
+ "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間",
+ "account_upgrade_dialog_tier_features_calls_one": "每日一通電話",
+ "account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話",
+ "account_upgrade_dialog_tier_features_emails_one": "每日一封郵件",
+ "account_upgrade_dialog_tier_features_emails_other": "每日 {{emails}} 條郵件",
+ "account_upgrade_dialog_tier_features_messages_one": "每日一條訊息",
+ "account_upgrade_dialog_tier_features_messages_other": "每日 {{messages}} 條訊息",
+ "account_upgrade_dialog_tier_features_no_calls": "沒有電話",
+ "account_upgrade_dialog_tier_features_no_reservations": "無保留主題",
+ "account_upgrade_dialog_tier_features_reservations_one": "保留一條主題",
+ "account_upgrade_dialog_tier_features_reservations_other": "保留 {{reservations}} 條主題",
+ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月計費。",
+ "account_upgrade_dialog_tier_price_billed_yearly": "{{價格}} 按年計費。節省 {{save}}。",
+ "account_upgrade_dialog_tier_price_per_month": "月",
+ "account_upgrade_dialog_tier_selected_label": "已選",
+ "account_upgrade_dialog_title": "更改帳戶等級",
+ "account_usage_attachment_storage_description": "每個文件 {{filesize}},在 {{expiry}} 後刪除",
+ "account_usage_attachment_storage_title": "附件存儲",
+ "account_usage_basis_ip_description": "此帳戶的使用統計資訊和限制基於你的 IP 地址,因此可能會與其他用戶共享。上面顯示的限制是基於現有速率限制的近似值。",
+ "account_usage_calls_none": "此帳號無法撥打電話",
+ "account_usage_calls_title": "已撥打電話",
+ "account_usage_cannot_create_portal_session": "無法打開計費門戶",
+ "account_usage_emails_title": "已發送電子郵件",
+ "account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置",
+ "account_usage_messages_title": "已發布訊息",
+ "account_usage_of_limit": "{{limit}} 的",
+ "account_usage_reservations_none": "此帳戶沒有保留主題",
+ "account_usage_reservations_title": "保留主題",
+ "account_usage_title": "使用量",
+ "account_usage_unlimited": "無限",
+ "action_bar_account": "帳戶",
+ "action_bar_change_display_name": "更改顯示名稱",
"action_bar_clear_notifications": "清除所有通知",
+ "action_bar_logo_alt": "ntfy 標識",
+ "action_bar_mute_notifications": "靜音",
+ "action_bar_profile_logout": "登出",
+ "action_bar_profile_settings": "設定",
+ "action_bar_profile_title": "個人資料",
+ "action_bar_reservation_add": "保留主題",
+ "action_bar_reservation_delete": "移除保留",
+ "action_bar_reservation_edit": "更改保留",
+ "action_bar_reservation_limit_reached": "達到限制",
+ "action_bar_send_test_notification": "發送測試通知",
+ "action_bar_settings": "設定",
"action_bar_show_menu": "顯示選單",
- "nav_button_documentation": "文件",
- "nav_button_publish_message": "發佈通知",
- "nav_button_muted": "通知已靜音",
- "notifications_copied_to_clipboard": "已複製到剪貼簿",
- "message_bar_publish": "發佈訊息",
- "message_bar_show_dialog": "顯示發佈對話框",
- "message_bar_error_publishing": "發佈通知時發生錯誤",
- "nav_topics_title": "訂閱主題",
- "nav_button_all_notifications": "所有通知",
+ "action_bar_sign_in": "登錄",
+ "action_bar_sign_up": "註冊",
+ "action_bar_toggle_action_menu": "開啟或關閉操作選單",
+ "action_bar_toggle_mute": "通知靜音/解除通知靜音",
+ "action_bar_unmute_notifications": "取消靜音",
+ "action_bar_unsubscribe": "取消訂閱",
+ "alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕",
+ "alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式",
+ "alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知。",
+ "alert_notification_permission_denied_title": "已禁用通知",
+ "alert_notification_permission_required_button": "現在授予",
+ "alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限。",
+ "alert_notification_permission_required_title": "已禁用通知",
+ "alert_not_supported_context_description": "通知僅支援 HTTPS。這是 Notifications API 的限制。",
+ "alert_not_supported_description": "你的瀏覽器不支援通知。",
+ "alert_not_supported_title": "不支援通知",
+ "common_add": "新增",
+ "common_back": "返回",
+ "common_cancel": "取消",
+ "common_copy_to_clipboard": "複製到剪貼板",
+ "common_save": "保存",
+ "display_name_dialog_description": "為訂閱列表中顯示的主題設置一個替代名稱。這有助於更輕鬆地識別名稱複雜的主題。",
+ "display_name_dialog_placeholder": "顯示名稱",
+ "display_name_dialog_title": "更改顯示名稱",
+ "emoji_picker_search_clear": "清除搜索",
+ "emoji_picker_search_placeholder": "查找表情符號",
+ "error_boundary_button_copy_stack_trace": "複製堆疊追踪",
+ "error_boundary_button_reload_ntfy": "重新加載 ntfy",
+ "error_boundary_description": "這顯然不應該發生。對此非常抱歉。 如果你有時間,請在GitHub上報告,或通過Discord或Matrix告訴我們。",
+ "error_boundary_gathering_info": "收集更多資訊……",
+ "error_boundary_stack_trace": "堆疊追踪",
+ "error_boundary_title": "天啊,ntfy 崩潰了",
+ "error_boundary_unsupported_indexeddb_description": "Ntfy Web應用程式需要IndexedDB才能運行,且你的瀏覽器在隱私瀏覽模式下不支援IndexedDB。