v0.4.5 - releases + update-check
This commit is contained in:
parent
7dfb6a0abb
commit
7dd83f0a88
9 changed files with 290 additions and 6 deletions
112
.forgejo/workflows/release.yml
Normal file
112
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Forgejo Actions — maak automatisch een Release bij een v*-tag.
|
||||||
|
#
|
||||||
|
# Bouwt de release-notes uit:
|
||||||
|
# 1) de bijbehorende sectie in CHANGELOG.md
|
||||||
|
# 2) een commit-overzicht sinds de vorige tag
|
||||||
|
# en publiceert die als Forgejo-Release via de API. Pre-release tags
|
||||||
|
# (bv. v0.8.4-beta1) worden als pre-release gemarkeerd.
|
||||||
|
#
|
||||||
|
# Token: gebruikt de automatische GITHUB_TOKEN van Forgejo Actions. Werkt dat
|
||||||
|
# niet (onvoldoende rechten), zet dan een secret RELEASE_TOKEN met repo-write.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: [self-hosted, prod]
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout (volledige historie + tags)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Release-notes opbouwen
|
||||||
|
id: notes
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
echo "TAG=${TAG}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# Pre-release? (tag bevat een suffix met streepje, bv. -beta1 / -rc.1)
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
echo "PRERELEASE=true" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "PRERELEASE=false" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NOTES="$(mktemp)"
|
||||||
|
# 1) Sectie uit CHANGELOG.md (van "# vX..." tot de volgende "# v...")
|
||||||
|
awk -v ver="$VERSION" '
|
||||||
|
$0 ~ ("^# v?" ver "([ \t).—-]|$)") {grab=1; print; next}
|
||||||
|
grab && /^# v/ {exit}
|
||||||
|
grab {print}
|
||||||
|
' CHANGELOG.md > "$NOTES" || true
|
||||||
|
|
||||||
|
# 2) Commit-overzicht sinds de vorige tag
|
||||||
|
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "## Commits"
|
||||||
|
if [ -n "$PREV" ]; then
|
||||||
|
git log --no-merges --pretty="- %s (%h)" "${PREV}..${TAG}"
|
||||||
|
echo ""
|
||||||
|
echo "**Volledige changelog:** ${PREV}...${TAG}"
|
||||||
|
else
|
||||||
|
git log --no-merges --pretty="- %s (%h)" "${TAG}"
|
||||||
|
fi
|
||||||
|
} >> "$NOTES"
|
||||||
|
|
||||||
|
echo "NOTES_FILE=${NOTES}" >> "$GITHUB_ENV"
|
||||||
|
echo "----- release notes -----"; cat "$NOTES"
|
||||||
|
|
||||||
|
- name: Release aanmaken via Forgejo API
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TOKEN="${{ secrets.RELEASE_TOKEN }}"
|
||||||
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}"; fi
|
||||||
|
|
||||||
|
# Bouw JSON-payload (body veilig ge-escaped via python)
|
||||||
|
python3 - "$TAG" "$NOTES_FILE" "$PRERELEASE" > payload.json <<'PY'
|
||||||
|
import json, sys
|
||||||
|
tag, notes_file, pre = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
body = open(notes_file, encoding="utf-8").read()
|
||||||
|
print(json.dumps({
|
||||||
|
"tag_name": tag,
|
||||||
|
"name": tag,
|
||||||
|
"body": body,
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": pre == "true",
|
||||||
|
}))
|
||||||
|
PY
|
||||||
|
|
||||||
|
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||||
|
code=$(curl -sS -o resp.json -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "$API")
|
||||||
|
echo "HTTP ${code}"; cat resp.json || true
|
||||||
|
|
||||||
|
if [ "$code" = "201" ]; then
|
||||||
|
echo "Release ${TAG} aangemaakt."
|
||||||
|
elif [ "$code" = "409" ]; then
|
||||||
|
# Bestaat al: update de bestaande release
|
||||||
|
ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/tags/${TAG}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")
|
||||||
|
if [ -n "$ID" ]; then
|
||||||
|
curl -fsS -X PATCH \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "${API}/${ID}" >/dev/null
|
||||||
|
echo "Bestaande release ${TAG} bijgewerkt."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Release aanmaken faalde (HTTP ${code})"; exit 1
|
||||||
|
fi
|
||||||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,3 +1,27 @@
|
||||||
|
# v0.4.5 — Releases & in-app update-check
|
||||||
|
|
||||||
|
## Nieuw in v0.4.5
|
||||||
|
|
||||||
|
### 🏷️ Automatische releases bij een tag
|
||||||
|
Push je een `v*`-tag, dan maakt de nieuwe `release.yml`-workflow automatisch een
|
||||||
|
Forgejo-**Release** aan met notes uit de bijbehorende `CHANGELOG.md`-sectie plus
|
||||||
|
een commit-overzicht sinds de vorige tag. Pre-release tags (bv. `v0.8.4-beta1`)
|
||||||
|
worden als pre-release gemarkeerd. Zo krijg je een Releases-pagina met duidelijke
|
||||||
|
changelog per versie, vergelijkbaar met GitHub Releases.
|
||||||
|
|
||||||
|
### 🔔 In-app "update beschikbaar"
|
||||||
|
Server Up vergelijkt de draaiende versie met de laatste release via de Forgejo/
|
||||||
|
GitHub Releases-API (semver-vergelijking, pre-releases optioneel). Is er een
|
||||||
|
nieuwere versie, dan verschijnt een melding in de topbar en een blok in
|
||||||
|
Instellingen → Updates met versie, release-notes en een link. Instelbaar via
|
||||||
|
`UPDATE_API_URL` (of env `SU_UPDATE_API`) en de optie "pre-releases meenemen".
|
||||||
|
|
||||||
|
### 🔖 Versiebeleid
|
||||||
|
Bump bij elke wijziging het centrale `VERSION`-bestand; tags zijn de bron voor
|
||||||
|
release-versies (`vX.Y.Z`, of `-beta`/`-rc` voor pre-releases).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.4.4 — Nette versienummers op alle builds
|
# v0.4.4 — Nette versienummers op alle builds
|
||||||
|
|
||||||
## Nieuw in v0.4.4
|
## Nieuw in v0.4.4
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.4
|
0.4.5
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ from flask import Flask, Response, jsonify, render_template, request
|
||||||
|
|
||||||
import core as cfg
|
import core as cfg
|
||||||
from core import audit, jobs, i18n
|
from core import audit, jobs, i18n
|
||||||
from core import docker, git, boilerplates
|
from core import docker, git, boilerplates, updater
|
||||||
from core.modules import Module, CORE, discover
|
from core.modules import Module, CORE, discover
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="static", template_folder="templates")
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
||||||
|
|
||||||
VERSION = os.environ.get("SU_VERSION", "0.4.4")
|
VERSION = os.environ.get("SU_VERSION", "0.4.5")
|
||||||
|
|
||||||
MODULES: list[Module] = []
|
MODULES: list[Module] = []
|
||||||
USER_MOD = APP / "modules"
|
USER_MOD = APP / "modules"
|
||||||
|
|
@ -133,6 +133,11 @@ def api_version():
|
||||||
return jsonify(version=VERSION)
|
return jsonify(version=VERSION)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/update")
|
||||||
|
def api_update():
|
||||||
|
return jsonify(updater.check(VERSION))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/docker/images")
|
@app.route("/api/docker/images")
|
||||||
def api_images():
|
def api_images():
|
||||||
return jsonify(images=docker.images())
|
return jsonify(images=docker.images())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ DEFAULTS = {
|
||||||
"LANGUAGE": "nl",
|
"LANGUAGE": "nl",
|
||||||
"THEME": "dark",
|
"THEME": "dark",
|
||||||
"WIZARD_DONE": False,
|
"WIZARD_DONE": False,
|
||||||
|
# Update-check: Forgejo/GitHub Releases-API die de laatste release teruggeeft.
|
||||||
|
# Leeg = uitgeschakeld. Voorbeeld (Forgejo):
|
||||||
|
# http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases
|
||||||
|
"UPDATE_API_URL": os.environ.get("SU_UPDATE_API", ""),
|
||||||
|
"UPDATE_INCLUDE_PRERELEASE": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
_path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json"))
|
_path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json"))
|
||||||
|
|
|
||||||
69
server-up/core/updater.py
Normal file
69
server-up/core/updater.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""Update-check tegen een Forgejo/GitHub Releases-API (stdlib only)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import core as cfg
|
||||||
|
|
||||||
|
_SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+.](.+))?$")
|
||||||
|
|
||||||
|
|
||||||
|
def _pre_key(pre: str):
|
||||||
|
parts = [p for p in re.split(r"[.+-]", pre) if p] if pre else []
|
||||||
|
return tuple((0, int(p)) if p.isdigit() else (1, p) for p in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(v: str):
|
||||||
|
"""Naar vergelijkbare tuple. Release sorteert hoger dan pre-release."""
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
|
m = _SEMVER.match(v.strip())
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
pre = m.group(4) or ""
|
||||||
|
return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
|
||||||
|
1 if not pre else 0, _pre_key(pre))
|
||||||
|
|
||||||
|
|
||||||
|
def check(current: str) -> dict:
|
||||||
|
c = cfg.load()
|
||||||
|
url = (c.get("UPDATE_API_URL") or "").strip()
|
||||||
|
include_pre = bool(c.get("UPDATE_INCLUDE_PRERELEASE"))
|
||||||
|
res = {"enabled": bool(url), "current": current, "latest": None,
|
||||||
|
"update_available": False, "url": "", "notes": "",
|
||||||
|
"prerelease": False, "error": ""}
|
||||||
|
if not url:
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url, headers={"Accept": "application/json", "User-Agent": "server-up"})
|
||||||
|
with urllib.request.urlopen(req, timeout=8) as r:
|
||||||
|
data = json.loads(r.read().decode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
res["error"] = f"{type(e).__name__}: {e}"
|
||||||
|
return res
|
||||||
|
|
||||||
|
rels = data if isinstance(data, list) else [data]
|
||||||
|
best, best_t = None, None
|
||||||
|
for rel in rels:
|
||||||
|
if not isinstance(rel, dict) or rel.get("draft"):
|
||||||
|
continue
|
||||||
|
if rel.get("prerelease") and not include_pre:
|
||||||
|
continue
|
||||||
|
t = _parse(rel.get("tag_name") or rel.get("name") or "")
|
||||||
|
if not t:
|
||||||
|
continue
|
||||||
|
if best_t is None or t > best_t:
|
||||||
|
best, best_t = rel, t
|
||||||
|
if not best:
|
||||||
|
return res
|
||||||
|
|
||||||
|
res["latest"] = (best.get("tag_name") or best.get("name") or "").lstrip("v")
|
||||||
|
res["url"] = best.get("html_url") or best.get("url") or ""
|
||||||
|
res["notes"] = best.get("body") or ""
|
||||||
|
res["prerelease"] = bool(best.get("prerelease"))
|
||||||
|
cur_t = _parse(current)
|
||||||
|
if cur_t and best_t and best_t > cur_t:
|
||||||
|
res["update_available"] = True
|
||||||
|
return res
|
||||||
|
|
@ -126,6 +126,11 @@ tailwind.config = {
|
||||||
<span class="w-px h-4 bg-bd dark:bg-bd-dark mx-1"></span>
|
<span class="w-px h-4 bg-bd dark:bg-bd-dark mx-1"></span>
|
||||||
<span class="mdi mdi-layers-outline text-bl"></span><span x-text="docker.images ?? '–'"></span>
|
<span class="mdi mdi-layers-outline text-bl"></span><span x-text="docker.images ?? '–'"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<a x-show="update.update_available" x-cloak :href="update.url" target="_blank" rel="noopener"
|
||||||
|
:title="t('update_available')"
|
||||||
|
class="flex items-center gap-1.5 px-3 h-10 rounded-xl bg-ac/10 text-ac dark:text-ac-dark text-xs font-bold hover:bg-ac/20 transition">
|
||||||
|
<span class="mdi mdi-arrow-up-bold-circle text-base"></span><span x-text="'v'+update.latest"></span>
|
||||||
|
</a>
|
||||||
<button @click="toggleTheme()" :title="t('theme')" class="btn-icon">
|
<button @click="toggleTheme()" :title="t('theme')" class="btn-icon">
|
||||||
<span class="mdi text-xl" :class="theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"></span>
|
<span class="mdi text-xl" :class="theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -360,6 +365,44 @@ tailwind.config = {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Updates -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
|
<span class="mdi mdi-update text-ac dark:text-ac-dark text-lg"></span>
|
||||||
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('updates')">Updates</span>
|
||||||
|
<template x-if="update.update_available"><span class="badge badge-ok" x-text="t('update_available')">update beschikbaar</span></template>
|
||||||
|
<template x-if="update.enabled && !update.update_available && !update.error"><span class="badge badge-dim" x-text="t('up_to_date')">up-to-date</span></template>
|
||||||
|
<button class="btn-sm ml-auto" @click="loadUpdate()"><span class="mdi mdi-refresh"></span> <span x-text="t('check_now')">Controleren</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div class="flex flex-wrap gap-x-8 gap-y-2 text-sm">
|
||||||
|
<div><div class="form-label" x-text="t('current_version')">Huidige versie</div><div class="font-mono font-semibold text-wh dark:text-wh-dark" x-text="'v'+(update.current||version)"></div></div>
|
||||||
|
<div x-show="update.latest"><div class="form-label" x-text="t('latest_version')">Laatste release</div><div class="font-mono font-semibold" :class="update.update_available ? 'text-ac dark:text-ac-dark' : 'text-wh dark:text-wh-dark'" x-text="'v'+update.latest"></div></div>
|
||||||
|
</div>
|
||||||
|
<template x-if="update.update_available && update.notes">
|
||||||
|
<details class="rounded-xl border border-bd dark:border-bd-dark overflow-hidden">
|
||||||
|
<summary class="px-3.5 py-2.5 cursor-pointer font-bold text-xs text-wh dark:text-wh-dark bg-s2/60 dark:bg-s2-dark/60 flex items-center gap-2"><span class="mdi mdi-text-box-outline"></span> <span x-text="t('release_notes')">Release notes</span></summary>
|
||||||
|
<pre class="p-3.5 text-xs whitespace-pre-wrap break-words text-t2 dark:text-t2-dark max-h-64 overflow-auto" x-text="update.notes"></pre>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
<template x-if="update.update_available">
|
||||||
|
<a :href="update.url" target="_blank" rel="noopener" class="btn btn-primary w-max"><span class="mdi mdi-open-in-new"></span> <span x-text="t('view_release')">Bekijk release</span></a>
|
||||||
|
</template>
|
||||||
|
<p x-show="update.error" class="text-xs text-er" x-text="update.error"></p>
|
||||||
|
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="form-label" x-text="t('update_api_url')">Releases-API URL</label>
|
||||||
|
<input class="form-input font-mono" x-model="settings.UPDATE_API_URL" @change="autoSave(); setTimeout(()=>loadUpdate(), 600)" placeholder="http://git.example.com/api/v1/repos/bes-r/server-up/releases">
|
||||||
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('update_api_hint')">Forgejo/GitHub Releases-API. Leeg = uit.</p>
|
||||||
|
</div>
|
||||||
|
<label class="inline-flex items-center gap-2.5 cursor-pointer">
|
||||||
|
<input type="checkbox" :checked="!!settings.UPDATE_INCLUDE_PRERELEASE" @change="settings.UPDATE_INCLUDE_PRERELEASE=$event.target.checked; autoSave(); setTimeout(()=>loadUpdate(), 600)" class="w-5 h-5 rounded border-bd dark:border-bd-dark text-ac">
|
||||||
|
<span class="text-sm text-tx dark:text-tx-dark" x-text="t('include_prerelease')">Pre-releases (beta) meenemen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Taalmodule (add-on) -->
|
<!-- Taalmodule (add-on) -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
|
|
@ -728,7 +771,7 @@ tailwind.config = {
|
||||||
<script>
|
<script>
|
||||||
function app() {
|
function app() {
|
||||||
return {
|
return {
|
||||||
version: '0.4.4',
|
version: '0.4.5',
|
||||||
page: location.hash.slice(1) || 'dashboard',
|
page: location.hash.slice(1) || 'dashboard',
|
||||||
isMobile: window.innerWidth < 768,
|
isMobile: window.innerWidth < 768,
|
||||||
sidebarOpen: window.innerWidth >= 768 && localStorage.getItem('sidebar') !== 'closed',
|
sidebarOpen: window.innerWidth >= 768 && localStorage.getItem('sidebar') !== 'closed',
|
||||||
|
|
@ -750,6 +793,7 @@ function app() {
|
||||||
settings: {},
|
settings: {},
|
||||||
languages: [],
|
languages: [],
|
||||||
modules: [],
|
modules: [],
|
||||||
|
update: { enabled:false, update_available:false, current:'', latest:'', url:'', notes:'', prerelease:false },
|
||||||
T: {},
|
T: {},
|
||||||
|
|
||||||
job: { id: null, lines: [], status: 'idle', timer: null },
|
job: { id: null, lines: [], status: 'idle', timer: null },
|
||||||
|
|
@ -794,6 +838,7 @@ function app() {
|
||||||
await this.loadDocker();
|
await this.loadDocker();
|
||||||
await this.loadModules();
|
await this.loadModules();
|
||||||
await this.loadDashboard();
|
await this.loadDashboard();
|
||||||
|
this.loadUpdate();
|
||||||
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
|
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
|
||||||
this.$watch('page', async (p) => {
|
this.$watch('page', async (p) => {
|
||||||
if (p === 'stacks') this.loadStacks();
|
if (p === 'stacks') this.loadStacks();
|
||||||
|
|
@ -1080,6 +1125,8 @@ function app() {
|
||||||
|
|
||||||
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
|
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
|
||||||
|
|
||||||
|
async loadUpdate() { const d = await this.rpc('/api/update'); if (d && typeof d === 'object') this.update = d; },
|
||||||
|
|
||||||
async checkWizard() {
|
async checkWizard() {
|
||||||
const d = await this.rpc('/api/wizard');
|
const d = await this.rpc('/api/wizard');
|
||||||
if (!d.done) {
|
if (!d.done) {
|
||||||
|
|
|
||||||
|
|
@ -104,5 +104,16 @@
|
||||||
"lang_name": "Name",
|
"lang_name": "Name",
|
||||||
"lang_flag": "Flag",
|
"lang_flag": "Flag",
|
||||||
"search_keys": "Search key or text…",
|
"search_keys": "Search key or text…",
|
||||||
"delete_language_confirm": "Are you sure you want to delete this language?"
|
"delete_language_confirm": "Are you sure you want to delete this language?",
|
||||||
|
"updates": "Updates",
|
||||||
|
"update_available": "Update available",
|
||||||
|
"up_to_date": "Up to date",
|
||||||
|
"check_now": "Check now",
|
||||||
|
"current_version": "Current version",
|
||||||
|
"latest_version": "Latest release",
|
||||||
|
"release_notes": "Release notes",
|
||||||
|
"view_release": "View release",
|
||||||
|
"update_api_url": "Releases API URL",
|
||||||
|
"update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.",
|
||||||
|
"include_prerelease": "Include pre-releases (beta)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,5 +104,16 @@
|
||||||
"lang_name": "Naam",
|
"lang_name": "Naam",
|
||||||
"lang_flag": "Vlag",
|
"lang_flag": "Vlag",
|
||||||
"search_keys": "Zoek vertaalsleutel of tekst…",
|
"search_keys": "Zoek vertaalsleutel of tekst…",
|
||||||
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?"
|
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?",
|
||||||
|
"updates": "Updates",
|
||||||
|
"update_available": "Update beschikbaar",
|
||||||
|
"up_to_date": "Up-to-date",
|
||||||
|
"check_now": "Controleren",
|
||||||
|
"current_version": "Huidige versie",
|
||||||
|
"latest_version": "Laatste release",
|
||||||
|
"release_notes": "Release notes",
|
||||||
|
"view_release": "Bekijk release",
|
||||||
|
"update_api_url": "Releases-API URL",
|
||||||
|
"update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.",
|
||||||
|
"include_prerelease": "Pre-releases (beta) meenemen"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue