Add Tauri auto-update functionality with in-app download

- Add tauri-plugin-updater and tauri-plugin-process dependencies
- Configure updater endpoint for GitHub Releases
- Rewrite UpdateModal with download progress and changelog display
- Add update-related i18n translations (15 languages)
- Configure createUpdaterArtifacts for .sig file generation
- Update workflow to use correct bundle types for each platform
This commit is contained in:
SuperKali
2025-12-19 14:54:22 +01:00
parent 2407ad9127
commit 3389e7fbd2
24 changed files with 790 additions and 81 deletions

View File

@@ -35,6 +35,9 @@ env:
CARGO_TERM_COLOR: always
NODE_VERSION: '20'
TAURI_CLI_VERSION: '^2'
# Tauri updater signing key (set in GitHub Secrets)
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
jobs:
create-release:
@@ -166,9 +169,9 @@ jobs:
fi
- name: Build Tauri app
run: cargo tauri build --bundles deb
run: cargo tauri build --bundles deb,appimage
- name: Upload .deb to GitHub Release
- name: Upload artifacts to GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.create-release.outputs.release_tag }}
@@ -180,6 +183,8 @@ jobs:
replacesArtifacts: false
artifacts: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
build-linux-arm64:
name: build-linux (aarch64-unknown-linux-gnu)
@@ -259,9 +264,9 @@ jobs:
fi
- name: Build Tauri app
run: cargo tauri build --bundles deb
run: cargo tauri build --bundles deb,appimage
- name: Upload .deb to GitHub Release
- name: Upload artifacts to GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.create-release.outputs.release_tag }}
@@ -273,6 +278,8 @@ jobs:
replacesArtifacts: false
artifacts: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
build-macos:
name: build-macos (${{ matrix.target }})
@@ -363,7 +370,7 @@ jobs:
run: |
cargo tauri build --target "${{ matrix.target }}" --bundles dmg,app
- name: Zip .app bundle(s)
- name: Zip .app bundle(s) and rename updater artifacts
shell: bash
env:
CARGO_TARGET_DIR: src-tauri/target/${{ matrix.target }}
@@ -374,6 +381,15 @@ jobs:
base="$(basename "$app" .app)"
ditto -c -k --sequesterRsrc --keepParent "$app" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.app.zip"
done
# Rename updater tar.gz to include arch
for tarball in "$CARGO_TARGET_DIR/release/bundle/macos/"*.tar.gz; do
base="$(basename "$tarball" .tar.gz)"
mv "$tarball" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz"
done
for sig in "$CARGO_TARGET_DIR/release/bundle/macos/"*.tar.gz.sig; do
base="$(basename "$sig" .tar.gz.sig)"
mv "$sig" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz.sig"
done
- name: Upload macOS artifacts to GitHub Release
uses: ncipollo/release-action@v1
@@ -387,6 +403,8 @@ jobs:
replacesArtifacts: false
artifacts: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*-${{ matrix.arch }}.app.zip
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*-${{ matrix.arch }}.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*-${{ matrix.arch }}.tar.gz.sig
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
build-windows-x64:
@@ -473,6 +491,8 @@ jobs:
artifacts: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
build-windows-arm64:
name: build-windows (aarch64-pc-windows-msvc)
@@ -559,6 +579,129 @@ jobs:
artifacts: |
src-tauri/target/aarch64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/aarch64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/aarch64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip
src-tauri/target/aarch64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
generate-update-manifest:
name: Generate latest.json for updater
needs:
- create-release
- build-linux-x64
- build-linux-arm64
- build-macos
- build-windows-x64
- build-windows-arm64
if: |
always() &&
(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download release assets and generate latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.create-release.outputs.release_tag }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
REPO="${{ github.repository }}"
BASE_URL="https://github.com/${REPO}/releases/download/${TAG}"
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Function to get signature content from release
get_sig() {
local asset_name="$1"
gh release download "$TAG" --pattern "${asset_name}.sig" --output - 2>/dev/null || echo ""
}
# Wait for assets to be available
sleep 10
# Get list of assets
ASSETS=$(gh release view "$TAG" --json assets --jq '.assets[].name')
# Initialize platforms object
declare -A PLATFORMS
# macOS x64
MAC_X64_SIG=$(get_sig "Armbian Imager-x64.tar.gz" || echo "")
if [[ -n "$MAC_X64_SIG" ]]; then
PLATFORMS["darwin-x86_64"]="{\"signature\": \"${MAC_X64_SIG}\", \"url\": \"${BASE_URL}/Armbian%20Imager-x64.tar.gz\"}"
fi
# macOS ARM64
MAC_ARM_SIG=$(get_sig "Armbian Imager-arm64.tar.gz" || echo "")
if [[ -n "$MAC_ARM_SIG" ]]; then
PLATFORMS["darwin-aarch64"]="{\"signature\": \"${MAC_ARM_SIG}\", \"url\": \"${BASE_URL}/Armbian%20Imager-arm64.tar.gz\"}"
fi
# Linux x64
LINUX_X64_SIG=$(get_sig "armbian-imager_${VERSION}_amd64.AppImage" || echo "")
if [[ -n "$LINUX_X64_SIG" ]]; then
PLATFORMS["linux-x86_64"]="{\"signature\": \"${LINUX_X64_SIG}\", \"url\": \"${BASE_URL}/armbian-imager_${VERSION}_amd64.AppImage\"}"
fi
# Linux ARM64
LINUX_ARM_SIG=$(get_sig "armbian-imager_${VERSION}_arm64.AppImage" || echo "")
if [[ -n "$LINUX_ARM_SIG" ]]; then
PLATFORMS["linux-aarch64"]="{\"signature\": \"${LINUX_ARM_SIG}\", \"url\": \"${BASE_URL}/armbian-imager_${VERSION}_arm64.AppImage\"}"
fi
# Windows x64
WIN_X64_SIG=$(get_sig "Armbian Imager_${VERSION}_x64-setup.nsis.zip" || echo "")
if [[ -n "$WIN_X64_SIG" ]]; then
PLATFORMS["windows-x86_64"]="{\"signature\": \"${WIN_X64_SIG}\", \"url\": \"${BASE_URL}/Armbian%20Imager_${VERSION}_x64-setup.nsis.zip\"}"
fi
# Windows ARM64
WIN_ARM_SIG=$(get_sig "Armbian Imager_${VERSION}_arm64-setup.nsis.zip" || echo "")
if [[ -n "$WIN_ARM_SIG" ]]; then
PLATFORMS["windows-aarch64"]="{\"signature\": \"${WIN_ARM_SIG}\", \"url\": \"${BASE_URL}/Armbian%20Imager_${VERSION}_arm64-setup.nsis.zip\"}"
fi
# Build platforms JSON
PLATFORMS_JSON="{"
first=true
for key in "${!PLATFORMS[@]}"; do
if [[ "$first" == "true" ]]; then
first=false
else
PLATFORMS_JSON+=","
fi
PLATFORMS_JSON+="\"${key}\": ${PLATFORMS[$key]}"
done
PLATFORMS_JSON+="}"
# Get release notes
NOTES=$(gh release view "$TAG" --json body --jq '.body' | jq -Rs .)
# Generate latest.json
cat > latest.json <<EOF
{
"version": "${VERSION}",
"notes": ${NOTES},
"pub_date": "${PUB_DATE}",
"platforms": ${PLATFORMS_JSON}
}
EOF
# Pretty print for debugging
cat latest.json | jq .
- name: Upload latest.json to release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.create-release.outputs.release_tag }}
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
artifacts: latest.json
finalize-release:
name: Publish release (draft -> false) + cleanup
@@ -569,6 +712,7 @@ jobs:
- build-macos
- build-windows-x64
- build-windows-arm64
- generate-update-manifest
if: |
always() &&
(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') &&

20
package-lock.json generated
View File

@@ -10,7 +10,9 @@
"dependencies": {
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-process": "^2",
"@tauri-apps/plugin-shell": "^2.3.3",
"@tauri-apps/plugin-updater": "^2",
"@types/qrcode": "^1.5.6",
"i18next": "^25.7.2",
"lucide-react": "^0.560.0",
@@ -1582,6 +1584,15 @@
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-process": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.3.tgz",
@@ -1591,6 +1602,15 @@
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
"integrity": "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View File

@@ -32,6 +32,8 @@
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-shell": "^2.3.3",
"@tauri-apps/plugin-updater": "^2",
"@tauri-apps/plugin-process": "^2",
"@types/qrcode": "^1.5.6",
"i18next": "^25.7.2",
"lucide-react": "^0.560.0",

View File

@@ -13,6 +13,8 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = ["protocol-asset", "macos-private-api"] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

View File

@@ -6,6 +6,8 @@
"permissions": [
"core:default",
"shell:allow-open",
"dialog:default"
"dialog:default",
"updater:default",
"process:allow-restart"
]
}

View File

@@ -57,6 +57,8 @@ fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![
commands::board_queries::get_boards,

View File

@@ -34,6 +34,7 @@
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -81,6 +82,15 @@
"plugins": {
"shell": {
"open": true
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYxQzgxMEQ3QUI1RjRDQ0UKUldUT1RGK3IxeERJOGJnSzNrVG1EZ2JVcVRGYllCdFBQNDFVa0l0WjF5NFNndk9YVzhlMDIrS04K",
"endpoints": [
"https://github.com/armbian/imager/releases/latest/download/latest.json"
],
"windows": {
"installMode": "passive"
}
}
}
}

View File

@@ -1,88 +1,276 @@
import { useState, useEffect } from 'react';
import { Download, RefreshCw } from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { Download, RefreshCw, CheckCircle, AlertCircle, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getVersion } from '@tauri-apps/api/app';
import { openUrl } from '../../hooks/useTauri';
import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
const GITHUB_API_URL = 'https://api.github.com/repos/armbian/imager/releases/latest';
const RELEASES_URL = 'https://github.com/armbian/imager/releases/latest';
type UpdateState = 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'error';
interface GitHubRelease {
tag_name: string;
html_url: string;
}
function compareVersions(current: string, latest: string): number {
const c = current.replace(/^v/, '').split('.').map(Number);
const l = latest.replace(/^v/, '').split('.').map(Number);
for (let i = 0; i < Math.max(c.length, l.length); i++) {
const cv = c[i] || 0;
const lv = l[i] || 0;
if (cv < lv) return -1;
if (cv > lv) return 1;
}
return 0;
interface DownloadProgress {
downloaded: number;
total: number | null;
}
export function UpdateModal() {
const { t } = useTranslation();
const [updateAvailable, setUpdateAvailable] = useState(false);
const [latestVersion, setLatestVersion] = useState<string | null>(null);
const [state, setState] = useState<UpdateState>('idle');
const [update, setUpdate] = useState<Update | null>(null);
const [progress, setProgress] = useState<DownloadProgress>({ downloaded: 0, total: null });
const [error, setError] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const checkForUpdate = async () => {
try {
const currentVersion = await getVersion();
const response = await fetch(GITHUB_API_URL);
const checkForUpdate = useCallback(async () => {
setState('checking');
setError(null);
if (!response.ok) return;
try {
const updateResult = await check();
const release: GitHubRelease = await response.json();
const latest = release.tag_name;
if (compareVersions(currentVersion, latest) < 0) {
setLatestVersion(latest);
setUpdateAvailable(true);
}
} catch (err) {
console.error('Failed to check for updates:', err);
if (updateResult) {
setUpdate(updateResult);
setState('available');
} else {
setState('idle');
}
};
checkForUpdate();
} catch (err) {
console.error('Failed to check for updates:', err);
// Silently fail - don't show error modal for update check failures
setState('idle');
}
}, []);
const handleDownload = () => {
openUrl(RELEASES_URL).catch(console.error);
setDismissed(true);
useEffect(() => {
checkForUpdate();
}, [checkForUpdate]);
const handleDownloadAndInstall = async () => {
if (!update) return;
setState('downloading');
setProgress({ downloaded: 0, total: null });
try {
await update.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
setProgress({ downloaded: 0, total: event.data.contentLength ?? null });
break;
case 'Progress':
setProgress((prev) => ({
...prev,
downloaded: prev.downloaded + event.data.chunkLength,
}));
break;
case 'Finished':
setState('ready');
break;
}
});
setState('ready');
} catch (err) {
console.error('Failed to download update:', err);
setError(err instanceof Error ? err.message : 'Download failed');
setState('error');
}
};
const handleRelaunch = async () => {
try {
await relaunch();
} catch (err) {
console.error('Failed to relaunch:', err);
setError(err instanceof Error ? err.message : 'Failed to restart');
setState('error');
}
};
const handleLater = () => {
setDismissed(true);
};
if (!updateAvailable || dismissed) return null;
const handleRetry = () => {
if (state === 'error' && update) {
handleDownloadAndInstall();
} else {
checkForUpdate();
}
};
const formatBytes = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
const getProgressPercentage = (): number => {
if (!progress.total) return 0;
return Math.round((progress.downloaded / progress.total) * 100);
};
// Simple markdown renderer for changelog
const renderChangelog = (body: string | undefined): React.ReactElement | null => {
if (!body) return null;
const lines = body.split('\n');
const elements: React.ReactElement[] = [];
lines.forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed) return;
if (trimmed.startsWith('## ')) {
elements.push(
<h4 key={index} className="update-changelog-heading">
{trimmed.slice(3)}
</h4>
);
} else if (trimmed.startsWith('### ')) {
elements.push(
<h5 key={index} className="update-changelog-subheading">
{trimmed.slice(4)}
</h5>
);
} else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
elements.push(
<li key={index} className="update-changelog-item">
{trimmed.slice(2)}
</li>
);
} else if (trimmed) {
elements.push(
<p key={index} className="update-changelog-text">
{trimmed}
</p>
);
}
});
return <>{elements}</>;
};
// Don't show anything if no update or dismissed
if (state === 'idle' || state === 'checking' || dismissed) return null;
return (
<div className="update-modal-overlay">
<div className="update-modal">
<div className="update-modal-icon">
<RefreshCw size={32} />
<div className={`update-modal ${state === 'available' ? 'update-modal-large' : ''}`}>
{/* Close button for available state */}
{state === 'available' && (
<button className="update-modal-close" onClick={handleLater} aria-label="Close">
<X size={18} />
</button>
)}
{/* Icon */}
<div className={`update-modal-icon ${state === 'ready' ? 'success' : ''} ${state === 'error' ? 'error' : ''}`}>
{state === 'ready' ? (
<CheckCircle size={32} />
) : state === 'error' ? (
<AlertCircle size={32} />
) : (
<RefreshCw size={32} className={state === 'downloading' ? 'spinning' : ''} />
)}
</div>
<h2 className="update-modal-title">{t('update.title')}</h2>
<p className="update-modal-message">
{t('update.newVersionAvailable', { version: latestVersion })}
</p>
{/* Title */}
<h2 className="update-modal-title">
{state === 'available' && t('update.title')}
{state === 'downloading' && t('update.downloading')}
{state === 'ready' && t('update.ready')}
{state === 'error' && t('update.error')}
</h2>
{/* Message / Content */}
{state === 'available' && update && (
<>
<div className="update-version-info">
<span className="update-version-current">{update.currentVersion}</span>
<span className="update-version-arrow"></span>
<span className="update-version-new">{update.version}</span>
</div>
{update.body && (
<div className="update-changelog">
<h3 className="update-changelog-title">{t('update.changelog')}</h3>
<div className="update-changelog-content">
{renderChangelog(update.body)}
</div>
</div>
)}
</>
)}
{state === 'downloading' && (
<div className="update-progress-container">
<div className="update-progress-bar">
<div
className="update-progress-fill"
style={{ width: `${getProgressPercentage()}%` }}
/>
</div>
<div className="update-progress-text">
{progress.total ? (
<>
{formatBytes(progress.downloaded)} / {formatBytes(progress.total)}
<span className="update-progress-percent">{getProgressPercentage()}%</span>
</>
) : (
formatBytes(progress.downloaded)
)}
</div>
</div>
)}
{state === 'ready' && (
<p className="update-modal-message">
{t('update.readyMessage')}
</p>
)}
{state === 'error' && (
<p className="update-modal-message update-error-message">
{error || t('update.errorMessage')}
</p>
)}
{/* Buttons */}
<div className="update-modal-buttons">
<button className="update-modal-btn secondary" onClick={handleLater}>
{t('update.later')}
</button>
<button className="update-modal-btn primary" onClick={handleDownload}>
<Download size={16} />
{t('update.download')}
</button>
{state === 'available' && (
<>
<button className="update-modal-btn secondary" onClick={handleLater}>
{t('update.later')}
</button>
<button className="update-modal-btn primary" onClick={handleDownloadAndInstall}>
<Download size={16} />
{t('update.installNow')}
</button>
</>
)}
{state === 'downloading' && (
<button className="update-modal-btn secondary" onClick={handleLater}>
{t('update.cancel')}
</button>
)}
{state === 'ready' && (
<button className="update-modal-btn primary" onClick={handleRelaunch}>
<RefreshCw size={16} />
{t('update.restartNow')}
</button>
)}
{state === 'error' && (
<>
<button className="update-modal-btn secondary" onClick={handleLater}>
{t('update.later')}
</button>
<button className="update-modal-btn primary" onClick={handleRetry}>
{t('update.retry')}
</button>
</>
)}
</div>
</div>
</div>

View File

@@ -103,6 +103,16 @@
"title": "Update verfügbar",
"newVersionAvailable": "Neue Version {{version}} verfügbar",
"download": "Herunterladen",
"later": "Später"
"later": "Später",
"downloading": "Update wird heruntergeladen...",
"ready": "Update bereit",
"error": "Update fehlgeschlagen",
"changelog": "Neuerungen",
"installNow": "Jetzt installieren",
"restartNow": "Jetzt neu starten",
"cancel": "Abbrechen",
"retry": "Erneut versuchen",
"readyMessage": "Das Update wurde heruntergeladen. Starten Sie die Anwendung neu, um die Installation abzuschließen.",
"errorMessage": "Das Update konnte nicht heruntergeladen werden. Bitte versuchen Sie es später erneut."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Update Available",
"newVersionAvailable": "New version {{version}} available",
"download": "Download",
"later": "Later"
"later": "Later",
"downloading": "Downloading Update...",
"ready": "Update Ready",
"error": "Update Failed",
"changelog": "What's New",
"installNow": "Install Now",
"restartNow": "Restart Now",
"cancel": "Cancel",
"retry": "Retry",
"readyMessage": "The update has been downloaded. Restart the application to complete the installation.",
"errorMessage": "Failed to download the update. Please try again later."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Actualización disponible",
"newVersionAvailable": "Nueva versión {{version}} disponible",
"download": "Descargar",
"later": "Más tarde"
"later": "Más tarde",
"downloading": "Descargando actualización...",
"ready": "Actualización lista",
"error": "Error de actualización",
"changelog": "Novedades",
"installNow": "Instalar ahora",
"restartNow": "Reiniciar ahora",
"cancel": "Cancelar",
"retry": "Reintentar",
"readyMessage": "La actualización se ha descargado. Reinicie la aplicación para completar la instalación.",
"errorMessage": "No se pudo descargar la actualización. Por favor, inténtelo más tarde."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Mise à jour disponible",
"newVersionAvailable": "Nouvelle version {{version}} disponible",
"download": "Télécharger",
"later": "Plus tard"
"later": "Plus tard",
"downloading": "Téléchargement de la mise à jour...",
"ready": "Mise à jour prête",
"error": "Échec de la mise à jour",
"changelog": "Nouveautés",
"installNow": "Installer maintenant",
"restartNow": "Redémarrer maintenant",
"cancel": "Annuler",
"retry": "Réessayer",
"readyMessage": "La mise à jour a été téléchargée. Redémarrez l'application pour terminer l'installation.",
"errorMessage": "Impossible de télécharger la mise à jour. Veuillez réessayer plus tard."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Aggiornamento Disponibile",
"newVersionAvailable": "Nuova versione {{version}} disponibile",
"download": "Scarica",
"later": "Più tardi"
"later": "Più tardi",
"downloading": "Download in corso...",
"ready": "Aggiornamento Pronto",
"error": "Aggiornamento Fallito",
"changelog": "Novità",
"installNow": "Installa Ora",
"restartNow": "Riavvia Ora",
"cancel": "Annulla",
"retry": "Riprova",
"readyMessage": "L'aggiornamento è stato scaricato. Riavvia l'applicazione per completare l'installazione.",
"errorMessage": "Impossibile scaricare l'aggiornamento. Riprova più tardi."
}
}

View File

@@ -103,6 +103,16 @@
"title": "アップデートが利用可能",
"newVersionAvailable": "新しいバージョン {{version}} が利用可能です",
"download": "ダウンロード",
"later": "後で"
"later": "後で",
"downloading": "アップデートをダウンロード中...",
"ready": "アップデート準備完了",
"error": "アップデート失敗",
"changelog": "新機能",
"installNow": "今すぐインストール",
"restartNow": "今すぐ再起動",
"cancel": "キャンセル",
"retry": "再試行",
"readyMessage": "アップデートがダウンロードされました。インストールを完了するにはアプリケーションを再起動してください。",
"errorMessage": "アップデートのダウンロードに失敗しました。後でもう一度お試しください。"
}
}

View File

@@ -103,6 +103,16 @@
"title": "업데이트 가능",
"newVersionAvailable": "새 버전 {{version}} 사용 가능",
"download": "다운로드",
"later": "나중에"
"later": "나중에",
"downloading": "업데이트 다운로드 중...",
"ready": "업데이트 준비됨",
"error": "업데이트 실패",
"changelog": "새로운 기능",
"installNow": "지금 설치",
"restartNow": "지금 재시작",
"cancel": "취소",
"retry": "다시 시도",
"readyMessage": "업데이트가 다운로드되었습니다. 설치를 완료하려면 애플리케이션을 재시작하세요.",
"errorMessage": "업데이트를 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Update beschikbaar",
"newVersionAvailable": "Nieuwe versie {{version}} beschikbaar",
"download": "Downloaden",
"later": "Later"
"later": "Later",
"downloading": "Update downloaden...",
"ready": "Update gereed",
"error": "Update mislukt",
"changelog": "Nieuw",
"installNow": "Nu installeren",
"restartNow": "Nu herstarten",
"cancel": "Annuleren",
"retry": "Opnieuw proberen",
"readyMessage": "De update is gedownload. Start de applicatie opnieuw om de installatie te voltooien.",
"errorMessage": "Kan de update niet downloaden. Probeer het later opnieuw."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Dostępna aktualizacja",
"newVersionAvailable": "Dostępna nowa wersja {{version}}",
"download": "Pobierz",
"later": "Później"
"later": "Później",
"downloading": "Pobieranie aktualizacji...",
"ready": "Aktualizacja gotowa",
"error": "Błąd aktualizacji",
"changelog": "Co nowego",
"installNow": "Zainstaluj teraz",
"restartNow": "Uruchom ponownie",
"cancel": "Anuluj",
"retry": "Spróbuj ponownie",
"readyMessage": "Aktualizacja została pobrana. Uruchom ponownie aplikację, aby zakończyć instalację.",
"errorMessage": "Nie udało się pobrać aktualizacji. Spróbuj ponownie później."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Atualização disponível",
"newVersionAvailable": "Nova versão {{version}} disponível",
"download": "Baixar",
"later": "Mais tarde"
"later": "Mais tarde",
"downloading": "Baixando atualização...",
"ready": "Atualização pronta",
"error": "Falha na atualização",
"changelog": "Novidades",
"installNow": "Instalar agora",
"restartNow": "Reiniciar agora",
"cancel": "Cancelar",
"retry": "Tentar novamente",
"readyMessage": "A atualização foi baixada. Reinicie o aplicativo para concluir a instalação.",
"errorMessage": "Não foi possível baixar a atualização. Por favor, tente novamente mais tarde."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Доступно обновление",
"newVersionAvailable": "Доступна новая версия {{version}}",
"download": "Скачать",
"later": "Позже"
"later": "Позже",
"downloading": "Загрузка обновления...",
"ready": "Обновление готово",
"error": "Ошибка обновления",
"changelog": "Что нового",
"installNow": "Установить сейчас",
"restartNow": "Перезапустить сейчас",
"cancel": "Отмена",
"retry": "Повторить",
"readyMessage": "Обновление загружено. Перезапустите приложение для завершения установки.",
"errorMessage": "Не удалось загрузить обновление. Пожалуйста, попробуйте позже."
}
}

View File

@@ -103,6 +103,16 @@
"title": "Na voljo posodobitev",
"newVersionAvailable": "Na voljo nova različica {{version}}",
"download": "Prenesi",
"later": "Kasneje"
"later": "Kasneje",
"downloading": "Prenašanje posodobitve...",
"ready": "Posodobitev pripravljena",
"error": "Posodobitev ni uspela",
"changelog": "Novosti",
"installNow": "Namesti zdaj",
"restartNow": "Znova zaženi",
"cancel": "Prekliči",
"retry": "Poskusi znova",
"readyMessage": "Posodobitev je prenesena. Znova zaženite aplikacijo za dokončanje namestitve.",
"errorMessage": "Posodobitve ni bilo mogoče prenesti. Poskusite pozneje."
}
}

Some files were not shown because too many files have changed in this diff Show More