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:
parent
c6576aca44
commit
214e22dc3d
24 changed files with 254 additions and 56 deletions
|
|
@ -587,11 +587,18 @@ Function .onInit
|
|||
|
||||
|
||||
; --- 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.
|
||||
; 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
|
||||
${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}
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
|
|
@ -757,6 +764,7 @@ Section Install
|
|||
; --- PORTABLE MODE --- Create portable marker and Data directory
|
||||
${If} $PortableMode = 1
|
||||
FileOpen $0 "$INSTDIR\portable" w
|
||||
FileWrite $0 "Handy Portable Mode"
|
||||
FileClose $0
|
||||
CreateDirectory "$INSTDIR\Data"
|
||||
DetailPrint "Portable mode: created marker file and Data directory."
|
||||
|
|
@ -1050,11 +1058,6 @@ Function CreateOrUpdateStartMenuShortcut
|
|||
FunctionEnd
|
||||
|
||||
Function CreateOrUpdateDesktopShortcut
|
||||
; --- PORTABLE MODE --- No desktop shortcut for portable installs
|
||||
${If} $PortableMode = 1
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
; We used to use product name as MAINBINARYNAME
|
||||
; migrate old shortcuts to target the new MAINBINARYNAME
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ pub fn cancel_operation(app: AppHandle) {
|
|||
cancel_current_operation(&app);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn is_portable() -> bool {
|
||||
crate::portable::is_portable()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ pub fn run(cli_args: CliArgs) {
|
|||
trigger_update_check,
|
||||
show_main_window_command,
|
||||
commands::cancel_operation,
|
||||
commands::is_portable,
|
||||
commands::get_app_dir_path,
|
||||
commands::get_app_settings,
|
||||
commands::get_default_settings,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,23 @@ pub fn init() {
|
|||
let exe_path = std::env::current_exe().ok()?;
|
||||
let exe_dir = exe_path.parent()?;
|
||||
|
||||
if exe_dir.join("portable").exists() {
|
||||
let data_dir = exe_dir.join("Data");
|
||||
let marker_path = exe_dir.join("portable");
|
||||
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() {
|
||||
std::fs::create_dir_all(&data_dir).ok()?;
|
||||
}
|
||||
|
|
@ -75,3 +90,77 @@ pub fn store_path(relative: &str) -> PathBuf {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,6 +426,9 @@ async showMainWindowCommand() : Promise<Result<null, string>> {
|
|||
async cancelOperation() : Promise<void> {
|
||||
await TAURI_INVOKE("cancel_operation");
|
||||
},
|
||||
async isPortable() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("is_portable");
|
||||
},
|
||||
async getAppDirPath() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") };
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { useTranslation } from "react-i18next";
|
|||
import { check } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { ProgressBar } from "../shared";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { commands } from "../../bindings";
|
||||
|
||||
interface UpdateCheckerProps {
|
||||
className?: string;
|
||||
|
|
@ -18,6 +20,8 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
|
|||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const [showUpToDate, setShowUpToDate] = useState(false);
|
||||
const [showPortableUpdateDialog, setShowPortableUpdateDialog] =
|
||||
useState(false);
|
||||
|
||||
const { settings, isLoading } = useSettings();
|
||||
const settingsLoaded = !isLoading && settings !== null;
|
||||
|
|
@ -97,6 +101,13 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
|
|||
|
||||
const installUpdate = async () => {
|
||||
if (!updateChecksEnabled) return;
|
||||
|
||||
const portable = await commands.isPortable();
|
||||
if (portable) {
|
||||
setShowPortableUpdateDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsInstalling(true);
|
||||
setDownloadProgress(0);
|
||||
|
|
@ -172,37 +183,68 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
|
|||
!isUpdateDisabled && (updateAvailable || (!isChecking && !showUpToDate));
|
||||
|
||||
return (
|
||||
<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>
|
||||
<>
|
||||
{showPortableUpdateDialog && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-bg border border-border rounded-lg p-6 max-w-md w-full mx-4 space-y-4">
|
||||
<h2 className="text-base font-semibold">
|
||||
{t("footer.portableUpdateTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text/70">
|
||||
{t("footer.portableUpdateMessage")}
|
||||
</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
className="px-3 py-1.5 text-sm rounded border border-border hover:bg-border/50 transition-colors"
|
||||
onClick={() => setShowPortableUpdateDialog(false)}
|
||||
>
|
||||
{t("common.close")}
|
||||
</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 && (
|
||||
<ProgressBar
|
||||
progress={[
|
||||
{
|
||||
id: "update",
|
||||
percentage: downloadProgress,
|
||||
},
|
||||
]}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isInstalling && downloadProgress > 0 && downloadProgress < 100 && (
|
||||
<ProgressBar
|
||||
progress={[
|
||||
{
|
||||
id: "update",
|
||||
percentage: downloadProgress,
|
||||
},
|
||||
]}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "...جاري التنزيل {{progress}}%",
|
||||
"installing": "...جاري التثبيت",
|
||||
"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": {
|
||||
"loading": "...جاري التحميل",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Stahování... {{progress}}%",
|
||||
"installing": "Instalace...",
|
||||
"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": {
|
||||
"loading": "Načítání...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Wird heruntergeladen... {{progress}}%",
|
||||
"installing": "Wird installiert...",
|
||||
"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": {
|
||||
"loading": "Wird geladen...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Downloading... {{progress}}%",
|
||||
"installing": "Installing...",
|
||||
"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": {
|
||||
"loading": "Loading...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Descargando... {{progress}}%",
|
||||
"installing": "Instalando...",
|
||||
"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": {
|
||||
"loading": "Cargando...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Téléchargement... {{progress}}%",
|
||||
"installing": "Installation...",
|
||||
"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": {
|
||||
"loading": "Chargement...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Download... {{progress}}%",
|
||||
"installing": "Installazione...",
|
||||
"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": {
|
||||
"loading": "Caricamento...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "ダウンロード中... {{progress}}%",
|
||||
"installing": "インストール中...",
|
||||
"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": {
|
||||
"loading": "読み込み中...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "다운로드 중... {{progress}}%",
|
||||
"installing": "설치 중...",
|
||||
"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": {
|
||||
"loading": "로딩 중...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Pobieranie... {{progress}}%",
|
||||
"installing": "Instalowanie...",
|
||||
"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": {
|
||||
"loading": "Wczytywanie...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Baixando... {{progress}}%",
|
||||
"installing": "Instalando...",
|
||||
"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": {
|
||||
"loading": "Carregando...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Загрузка... {{progress}} %",
|
||||
"installing": "Установка...",
|
||||
"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": {
|
||||
"loading": "Загрузка...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Laddar ner... {{progress}}%",
|
||||
"installing": "Installerar...",
|
||||
"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": {
|
||||
"loading": "Laddar...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "İndiriliyor... %{{progress}}",
|
||||
"installing": "Yükleniyor...",
|
||||
"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": {
|
||||
"loading": "Yükleniyor...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Завантаження... {{progress}}%",
|
||||
"installing": "Встановлення...",
|
||||
"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": {
|
||||
"loading": "Завантаження...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "Đang tải... {{progress}}%",
|
||||
"installing": "Đang cài đặt...",
|
||||
"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": {
|
||||
"loading": "Đang tải...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "下載中... {{progress}}%",
|
||||
"installing": "安裝中...",
|
||||
"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": {
|
||||
"loading": "載入中...",
|
||||
|
|
|
|||
|
|
@ -542,7 +542,10 @@
|
|||
"downloading": "下载中... {{progress}}%",
|
||||
"installing": "安装中...",
|
||||
"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": {
|
||||
"loading": "加载中...",
|
||||
|
|
|
|||
Loading…
Reference in a new issue