fix: require magic string in portable marker to prevent false portable mode on scoop installs (#1126)

* fix: require magic string in portable marker to prevent false portable mode

Scoop's extras bucket extracts the NSIS installer via #/dl.7z, which can
leave a stale empty portable file next to the exe. This caused
portable::init() to falsely trigger portable mode on non-portable scoop
installs, storing data in Data/ next to the exe (wiped on each update).

Changes:
- portable.rs: only enable portable mode when marker contains
  "Handy Portable Mode"; extracted is_valid_portable_marker() with tests
- installer.nsi: write magic string when creating marker file
- installer.nsi: validate magic string in update auto-detect block
- installer.nsi: allow desktop shortcut creation from finish page
  checkbox in portable mode (was silently doing nothing)
- commands/mod.rs + lib.rs: expose is_portable() as a Tauri command
- UpdateChecker.tsx: show manual-update popup for portable installs
  instead of attempting auto-install (which would download MSI)
- en/translation.json: add i18n strings for portable update popup

Fixes #1124

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add missing translation keys to all locales and fix prettier formatting

Add portableUpdateTitle, portableUpdateMessage, portableUpdateButton to
all 17 non-English locales (English fallback text) so check:translations
passes. Also run prettier on the PR's modified files to fix format:check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: migrate legacy empty portable markers from v0.8.0

v0.8.0 created an empty `portable` marker file. Users who manually
update (drop new exe in place) would silently lose portable mode since
the new magic-string check rejects empty files.

Migration: if the marker exists but has invalid/empty content AND a
`Data/` directory is present, treat it as a real portable install and
rewrite the marker with the magic string. This handles the v0.8.0 →
new upgrade path without regressing the scoop false-positive fix
(scoop does not create a Data/ directory).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shehab Tarek 2026-03-26 02:52:33 +02:00 committed by GitHub
parent c6576aca44
commit 214e22dc3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 254 additions and 56 deletions

View file

@ -587,11 +587,18 @@ Function .onInit
; --- PORTABLE MODE --- Auto-detect portable mode during updates. ; --- PORTABLE MODE --- Auto-detect portable mode during updates.
; If the target directory already has a portable marker file, preserve ; If the target directory already has a valid portable marker file, preserve
; portable mode so the Tauri updater works without needing /PORTABLE. ; portable mode so the Tauri updater works without needing /PORTABLE.
; We validate the magic string to avoid false-positives from stale empty files
; left by scoop's NSIS extraction (dl.7z side-effect).
${If} $PortableMode <> 1 ${If} $PortableMode <> 1
${AndIf} ${FileExists} "$INSTDIR\portable" ${AndIf} ${FileExists} "$INSTDIR\portable"
StrCpy $PortableMode 1 FileOpen $1 "$INSTDIR\portable" r
FileRead $1 $2
FileClose $1
${If} $2 == "Handy Portable Mode"
StrCpy $PortableMode 1
${EndIf}
${EndIf} ${EndIf}
!if "${INSTALLMODE}" == "both" !if "${INSTALLMODE}" == "both"
@ -757,6 +764,7 @@ Section Install
; --- PORTABLE MODE --- Create portable marker and Data directory ; --- PORTABLE MODE --- Create portable marker and Data directory
${If} $PortableMode = 1 ${If} $PortableMode = 1
FileOpen $0 "$INSTDIR\portable" w FileOpen $0 "$INSTDIR\portable" w
FileWrite $0 "Handy Portable Mode"
FileClose $0 FileClose $0
CreateDirectory "$INSTDIR\Data" CreateDirectory "$INSTDIR\Data"
DetailPrint "Portable mode: created marker file and Data directory." DetailPrint "Portable mode: created marker file and Data directory."
@ -1050,11 +1058,6 @@ Function CreateOrUpdateStartMenuShortcut
FunctionEnd FunctionEnd
Function CreateOrUpdateDesktopShortcut Function CreateOrUpdateDesktopShortcut
; --- PORTABLE MODE --- No desktop shortcut for portable installs
${If} $PortableMode = 1
Return
${EndIf}
; We used to use product name as MAINBINARYNAME ; We used to use product name as MAINBINARYNAME
; migrate old shortcuts to target the new MAINBINARYNAME ; migrate old shortcuts to target the new MAINBINARYNAME
!insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName"

View file

@ -14,6 +14,12 @@ pub fn cancel_operation(app: AppHandle) {
cancel_current_operation(&app); cancel_current_operation(&app);
} }
#[tauri::command]
#[specta::specta]
pub fn is_portable() -> bool {
crate::portable::is_portable()
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> { pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {

View file

@ -375,6 +375,7 @@ pub fn run(cli_args: CliArgs) {
trigger_update_check, trigger_update_check,
show_main_window_command, show_main_window_command,
commands::cancel_operation, commands::cancel_operation,
commands::is_portable,
commands::get_app_dir_path, commands::get_app_dir_path,
commands::get_app_settings, commands::get_app_settings,
commands::get_default_settings, commands::get_default_settings,

View file

@ -17,8 +17,23 @@ pub fn init() {
let exe_path = std::env::current_exe().ok()?; let exe_path = std::env::current_exe().ok()?;
let exe_dir = exe_path.parent()?; let exe_dir = exe_path.parent()?;
if exe_dir.join("portable").exists() { let marker_path = exe_dir.join("portable");
let data_dir = exe_dir.join("Data"); let data_dir = exe_dir.join("Data");
let is_portable = if is_valid_portable_marker(&marker_path) {
true
} else if marker_path.exists() && data_dir.exists() {
// Migration: v0.8.0 created an empty marker file. If we find an
// empty/invalid marker alongside an existing Data/ dir, this is a
// real portable install — upgrade the marker in place.
eprintln!("[portable] upgrading legacy empty marker to magic string");
let _ = std::fs::write(&marker_path, "Handy Portable Mode");
true
} else {
false
};
if is_portable {
if !data_dir.exists() { if !data_dir.exists() {
std::fs::create_dir_all(&data_dir).ok()?; std::fs::create_dir_all(&data_dir).ok()?;
} }
@ -75,3 +90,77 @@ pub fn store_path(relative: &str) -> PathBuf {
PathBuf::from(relative) PathBuf::from(relative)
} }
} }
/// Check if a marker file path contains the portable magic string.
/// Extracted for testability.
fn is_valid_portable_marker(path: &std::path::Path) -> bool {
std::fs::read_to_string(path)
.map(|s| s.trim().starts_with("Handy Portable Mode"))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_valid_magic_string_enables_portable() {
let dir = std::env::temp_dir().join("handy_test_valid");
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("portable");
let mut f = std::fs::File::create(&marker).unwrap();
write!(f, "Handy Portable Mode").unwrap();
assert!(is_valid_portable_marker(&marker));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_empty_file_does_not_enable_portable() {
let dir = std::env::temp_dir().join("handy_test_empty");
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("portable");
std::fs::File::create(&marker).unwrap();
assert!(!is_valid_portable_marker(&marker));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_wrong_content_does_not_enable_portable() {
let dir = std::env::temp_dir().join("handy_test_wrong");
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("portable");
let mut f = std::fs::File::create(&marker).unwrap();
write!(f, "some other content").unwrap();
assert!(!is_valid_portable_marker(&marker));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_missing_file_does_not_enable_portable() {
let path = std::path::Path::new("/nonexistent/portable");
assert!(!is_valid_portable_marker(path));
}
#[test]
fn test_legacy_empty_marker_without_data_dir_does_not_enable_portable() {
// Empty marker alone (scoop scenario) — no Data/ dir → not portable
let dir = std::env::temp_dir().join("handy_test_legacy_no_data");
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("portable");
std::fs::File::create(&marker).unwrap();
assert!(!is_valid_portable_marker(&marker));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_magic_string_with_whitespace_enables_portable() {
let dir = std::env::temp_dir().join("handy_test_ws");
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("portable");
let mut f = std::fs::File::create(&marker).unwrap();
write!(f, " Handy Portable Mode\n").unwrap();
assert!(is_valid_portable_marker(&marker));
std::fs::remove_dir_all(dir).unwrap();
}
}

View file

@ -426,6 +426,9 @@ async showMainWindowCommand() : Promise<Result<null, string>> {
async cancelOperation() : Promise<void> { async cancelOperation() : Promise<void> {
await TAURI_INVOKE("cancel_operation"); await TAURI_INVOKE("cancel_operation");
}, },
async isPortable() : Promise<boolean> {
return await TAURI_INVOKE("is_portable");
},
async getAppDirPath() : Promise<Result<string, string>> { async getAppDirPath() : Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") }; return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") };

View file

@ -3,8 +3,10 @@ import { useTranslation } from "react-i18next";
import { check } from "@tauri-apps/plugin-updater"; import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process"; import { relaunch } from "@tauri-apps/plugin-process";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { ProgressBar } from "../shared"; import { ProgressBar } from "../shared";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { commands } from "../../bindings";
interface UpdateCheckerProps { interface UpdateCheckerProps {
className?: string; className?: string;
@ -18,6 +20,8 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
const [isInstalling, setIsInstalling] = useState(false); const [isInstalling, setIsInstalling] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0); const [downloadProgress, setDownloadProgress] = useState(0);
const [showUpToDate, setShowUpToDate] = useState(false); const [showUpToDate, setShowUpToDate] = useState(false);
const [showPortableUpdateDialog, setShowPortableUpdateDialog] =
useState(false);
const { settings, isLoading } = useSettings(); const { settings, isLoading } = useSettings();
const settingsLoaded = !isLoading && settings !== null; const settingsLoaded = !isLoading && settings !== null;
@ -97,6 +101,13 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
const installUpdate = async () => { const installUpdate = async () => {
if (!updateChecksEnabled) return; if (!updateChecksEnabled) return;
const portable = await commands.isPortable();
if (portable) {
setShowPortableUpdateDialog(true);
return;
}
try { try {
setIsInstalling(true); setIsInstalling(true);
setDownloadProgress(0); setDownloadProgress(0);
@ -172,37 +183,68 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
!isUpdateDisabled && (updateAvailable || (!isChecking && !showUpToDate)); !isUpdateDisabled && (updateAvailable || (!isChecking && !showUpToDate));
return ( return (
<div className={`flex items-center gap-3 ${className}`}> <>
{isUpdateClickable ? ( {showPortableUpdateDialog && (
<button <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
onClick={getUpdateStatusAction()} <div className="bg-bg border border-border rounded-lg p-6 max-w-md w-full mx-4 space-y-4">
disabled={isUpdateDisabled} <h2 className="text-base font-semibold">
className={`transition-colors disabled:opacity-50 tabular-nums ${ {t("footer.portableUpdateTitle")}
updateAvailable </h2>
? "text-logo-primary hover:text-logo-primary/80 font-medium" <p className="text-sm text-text/70">
: "text-text/60 hover:text-text/80" {t("footer.portableUpdateMessage")}
}`} </p>
> <div className="flex gap-2 justify-end">
{getUpdateStatusText()} <button
</button> className="px-3 py-1.5 text-sm rounded border border-border hover:bg-border/50 transition-colors"
) : ( onClick={() => setShowPortableUpdateDialog(false)}
<span className="text-text/60 tabular-nums"> >
{getUpdateStatusText()} {t("common.close")}
</span> </button>
<button
className="px-3 py-1.5 text-sm rounded bg-logo-primary text-white hover:bg-logo-primary/80 transition-colors"
onClick={() => {
openUrl("https://github.com/cjpais/Handy/releases/latest");
setShowPortableUpdateDialog(false);
}}
>
{t("footer.portableUpdateButton")}
</button>
</div>
</div>
</div>
)} )}
<div className={`flex items-center gap-3 ${className}`}>
{isUpdateClickable ? (
<button
onClick={getUpdateStatusAction()}
disabled={isUpdateDisabled}
className={`transition-colors disabled:opacity-50 tabular-nums ${
updateAvailable
? "text-logo-primary hover:text-logo-primary/80 font-medium"
: "text-text/60 hover:text-text/80"
}`}
>
{getUpdateStatusText()}
</button>
) : (
<span className="text-text/60 tabular-nums">
{getUpdateStatusText()}
</span>
)}
{isInstalling && downloadProgress > 0 && downloadProgress < 100 && ( {isInstalling && downloadProgress > 0 && downloadProgress < 100 && (
<ProgressBar <ProgressBar
progress={[ progress={[
{ {
id: "update", id: "update",
percentage: downloadProgress, percentage: downloadProgress,
}, },
]} ]}
size="large" size="large"
/> />
)} )}
</div> </div>
</>
); );
}; };

View file

@ -542,7 +542,10 @@
"downloading": "...جاري التنزيل {{progress}}%", "downloading": "...جاري التنزيل {{progress}}%",
"installing": "...جاري التثبيت", "installing": "...جاري التثبيت",
"preparing": "...جاري التحضير", "preparing": "...جاري التحضير",
"checkForUpdates": "التحقق من وجود تحديثات" "checkForUpdates": "التحقق من وجود تحديثات",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "...جاري التحميل", "loading": "...جاري التحميل",

View file

@ -542,7 +542,10 @@
"downloading": "Stahování... {{progress}}%", "downloading": "Stahování... {{progress}}%",
"installing": "Instalace...", "installing": "Instalace...",
"preparing": "Příprava...", "preparing": "Příprava...",
"checkForUpdates": "Zkontrolovat aktualizace" "checkForUpdates": "Zkontrolovat aktualizace",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Načítání...", "loading": "Načítání...",

View file

@ -542,7 +542,10 @@
"downloading": "Wird heruntergeladen... {{progress}}%", "downloading": "Wird heruntergeladen... {{progress}}%",
"installing": "Wird installiert...", "installing": "Wird installiert...",
"preparing": "Wird vorbereitet...", "preparing": "Wird vorbereitet...",
"checkForUpdates": "Nach Updates suchen" "checkForUpdates": "Nach Updates suchen",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Wird geladen...", "loading": "Wird geladen...",

View file

@ -542,7 +542,10 @@
"downloading": "Downloading... {{progress}}%", "downloading": "Downloading... {{progress}}%",
"installing": "Installing...", "installing": "Installing...",
"preparing": "Preparing...", "preparing": "Preparing...",
"checkForUpdates": "Check for updates" "checkForUpdates": "Check for updates",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Loading...", "loading": "Loading...",

View file

@ -542,7 +542,10 @@
"downloading": "Descargando... {{progress}}%", "downloading": "Descargando... {{progress}}%",
"installing": "Instalando...", "installing": "Instalando...",
"preparing": "Preparando...", "preparing": "Preparando...",
"checkForUpdates": "Buscar actualizaciones" "checkForUpdates": "Buscar actualizaciones",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Cargando...", "loading": "Cargando...",

View file

@ -542,7 +542,10 @@
"downloading": "Téléchargement... {{progress}}%", "downloading": "Téléchargement... {{progress}}%",
"installing": "Installation...", "installing": "Installation...",
"preparing": "Préparation...", "preparing": "Préparation...",
"checkForUpdates": "Rechercher des mises à jour" "checkForUpdates": "Rechercher des mises à jour",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Chargement...", "loading": "Chargement...",

View file

@ -542,7 +542,10 @@
"downloading": "Download... {{progress}}%", "downloading": "Download... {{progress}}%",
"installing": "Installazione...", "installing": "Installazione...",
"preparing": "Preparazione...", "preparing": "Preparazione...",
"checkForUpdates": "Controlla aggiornamenti" "checkForUpdates": "Controlla aggiornamenti",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Caricamento...", "loading": "Caricamento...",

View file

@ -542,7 +542,10 @@
"downloading": "ダウンロード中... {{progress}}%", "downloading": "ダウンロード中... {{progress}}%",
"installing": "インストール中...", "installing": "インストール中...",
"preparing": "準備中...", "preparing": "準備中...",
"checkForUpdates": "アップデートを確認" "checkForUpdates": "アップデートを確認",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "読み込み中...", "loading": "読み込み中...",

View file

@ -542,7 +542,10 @@
"downloading": "다운로드 중... {{progress}}%", "downloading": "다운로드 중... {{progress}}%",
"installing": "설치 중...", "installing": "설치 중...",
"preparing": "준비 중...", "preparing": "준비 중...",
"checkForUpdates": "업데이트 확인" "checkForUpdates": "업데이트 확인",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "로딩 중...", "loading": "로딩 중...",

View file

@ -542,7 +542,10 @@
"downloading": "Pobieranie... {{progress}}%", "downloading": "Pobieranie... {{progress}}%",
"installing": "Instalowanie...", "installing": "Instalowanie...",
"preparing": "Przygotowywanie...", "preparing": "Przygotowywanie...",
"checkForUpdates": "Sprawdź aktualizacje" "checkForUpdates": "Sprawdź aktualizacje",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Wczytywanie...", "loading": "Wczytywanie...",

View file

@ -542,7 +542,10 @@
"downloading": "Baixando... {{progress}}%", "downloading": "Baixando... {{progress}}%",
"installing": "Instalando...", "installing": "Instalando...",
"preparing": "Preparando...", "preparing": "Preparando...",
"checkForUpdates": "Verificar atualizações" "checkForUpdates": "Verificar atualizações",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Carregando...", "loading": "Carregando...",

View file

@ -542,7 +542,10 @@
"downloading": "Загрузка... {{progress}} %", "downloading": "Загрузка... {{progress}} %",
"installing": "Установка...", "installing": "Установка...",
"preparing": "Подготовка...", "preparing": "Подготовка...",
"checkForUpdates": "Проверить наличие обновлений" "checkForUpdates": "Проверить наличие обновлений",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Загрузка...", "loading": "Загрузка...",

View file

@ -542,7 +542,10 @@
"downloading": "Laddar ner... {{progress}}%", "downloading": "Laddar ner... {{progress}}%",
"installing": "Installerar...", "installing": "Installerar...",
"preparing": "Förbereder...", "preparing": "Förbereder...",
"checkForUpdates": "Sök efter uppdateringar" "checkForUpdates": "Sök efter uppdateringar",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Laddar...", "loading": "Laddar...",

View file

@ -542,7 +542,10 @@
"downloading": "İndiriliyor... %{{progress}}", "downloading": "İndiriliyor... %{{progress}}",
"installing": "Yükleniyor...", "installing": "Yükleniyor...",
"preparing": "Hazırlanıyor...", "preparing": "Hazırlanıyor...",
"checkForUpdates": "Güncellemeleri kontrol et" "checkForUpdates": "Güncellemeleri kontrol et",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Yükleniyor...", "loading": "Yükleniyor...",

View file

@ -542,7 +542,10 @@
"downloading": "Завантаження... {{progress}}%", "downloading": "Завантаження... {{progress}}%",
"installing": "Встановлення...", "installing": "Встановлення...",
"preparing": "Підготовка...", "preparing": "Підготовка...",
"checkForUpdates": "Перевірити оновлення" "checkForUpdates": "Перевірити оновлення",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Завантаження...", "loading": "Завантаження...",

View file

@ -542,7 +542,10 @@
"downloading": "Đang tải... {{progress}}%", "downloading": "Đang tải... {{progress}}%",
"installing": "Đang cài đặt...", "installing": "Đang cài đặt...",
"preparing": "Đang chuẩn bị...", "preparing": "Đang chuẩn bị...",
"checkForUpdates": "Kiểm tra cập nhật" "checkForUpdates": "Kiểm tra cập nhật",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "Đang tải...", "loading": "Đang tải...",

View file

@ -542,7 +542,10 @@
"downloading": "下載中... {{progress}}%", "downloading": "下載中... {{progress}}%",
"installing": "安裝中...", "installing": "安裝中...",
"preparing": "準備中...", "preparing": "準備中...",
"checkForUpdates": "檢查更新" "checkForUpdates": "檢查更新",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "載入中...", "loading": "載入中...",

View file

@ -542,7 +542,10 @@
"downloading": "下载中... {{progress}}%", "downloading": "下载中... {{progress}}%",
"installing": "安装中...", "installing": "安装中...",
"preparing": "准备中...", "preparing": "准备中...",
"checkForUpdates": "检查更新" "checkForUpdates": "检查更新",
"portableUpdateTitle": "Manual update required",
"portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.",
"portableUpdateButton": "Open GitHub Releases"
}, },
"common": { "common": {
"loading": "加载中...", "loading": "加载中...",