From 45edebe8514a4433cb142ac4621b8e52f8e6d5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Wed, 18 Mar 2026 19:26:14 +0800 Subject: [PATCH] fix: add manual download option for macOS users with old code signing (#13378) ### What this PR does Before this PR: - macOS users with the old code signing Team ID (Q24M7JR***) cannot auto-update to v1.8.0+ due to signing certificate change - Update request headers lacked app identification information After this PR: - Detects old Team ID on macOS and shows a manual download button directing users to the official website - Adds `App-Name`, `App-Version`, and `OS` headers to update requests for better server analytics - Refactors duplicate headers into a shared `getCommonHeaders()` function ### Why we need it and why it was done in this way The following tradeoffs were made: - Using `codesign -dv` to detect the Team ID at runtime is simple and reliable on macOS The following alternatives were considered: - Checking the app version instead of Team ID, but this wouldn't correctly identify users who haven't updated in a while ### Breaking changes None ### Special notes for your reviewer The old Team ID `Q24M7JR2C4` is hardcoded as a constant. This is intentional since it represents the previous signing certificate that will no longer be used. ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Refactor: You have left the code cleaner than you found it (Boy Scout Rule) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A user-guide update was considered and is present (link) or not required - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note Add manual download option for macOS users who cannot auto-update due to code signing certificate change ``` --------- Signed-off-by: kangfenmao --- biome.jsonc | 2 +- config/app-upgrade-segments.json | 28 +++- packages/shared/IpcChannel.ts | 2 + packages/shared/config/constant.ts | 2 + src/main/ipc.ts | 3 + src/main/services/AppService.ts | 45 ++++++ src/main/services/AppUpdater.ts | 144 ++++++++++++++++-- src/preload/index.ts | 4 + .../components/Popups/UpdateDialogPopup.tsx | 75 ++++++++- src/renderer/src/i18n/locales/en-us.json | 4 + src/renderer/src/i18n/locales/zh-cn.json | 4 + src/renderer/src/i18n/locales/zh-tw.json | 4 + src/renderer/src/i18n/translate/de-de.json | 4 + src/renderer/src/i18n/translate/el-gr.json | 4 + src/renderer/src/i18n/translate/es-es.json | 4 + src/renderer/src/i18n/translate/fr-fr.json | 4 + src/renderer/src/i18n/translate/ja-jp.json | 4 + src/renderer/src/i18n/translate/pt-pt.json | 4 + src/renderer/src/i18n/translate/ro-ro.json | 4 + src/renderer/src/i18n/translate/ru-ru.json | 4 + 20 files changed, 329 insertions(+), 20 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 6bee38a64e..8d1deb195d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -23,7 +23,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**"], + "includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**", "!config/app-upgrade-segments.json"], "maxSize": 2097152 }, "formatter": { diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json index 70c8ac25f0..3eab6fa89c 100644 --- a/config/app-upgrade-segments.json +++ b/config/app-upgrade-segments.json @@ -1,18 +1,36 @@ { "segments": [ { - "id": "legacy-v1", + "id": "legacy-v1-locked", "type": "legacy", "match": { - "range": ">=1.0.0 <2.0.0" + "range": ">=1.0.0 <1.8.1" }, + "lockedVersion": "1.8.1", "minCompatibleVersion": "1.0.0", - "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "description": "Gateway version for users below v1.8.1 - locked to v1.8.1", "channelTemplates": { "latest": { "feedTemplates": { "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", - "gitcode": "https://releases.cherry-ai.com" + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + }, + { + "id": "current-v1", + "type": "latest", + "match": { + "range": ">=1.8.1 <2.0.0" + }, + "minCompatibleVersion": "1.8.1", + "description": "Current latest v1.x release for users on v1.8.1+", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" } }, "rc": { @@ -36,7 +54,7 @@ "exact": ["2.0.0"] }, "lockedVersion": "2.0.0", - "minCompatibleVersion": "1.7.0", + "minCompatibleVersion": "1.9.0", "description": "Major release v2.0 - required intermediate version for v2.x upgrades", "channelTemplates": { "latest": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index e16ac5e50f..8bb5679eca 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -7,9 +7,11 @@ export enum IpcChannel { App_SetSpellCheckLanguages = 'app:set-spell-check-languages', App_CheckForUpdate = 'app:check-for-update', App_QuitAndInstall = 'app:quit-and-install', + App_ManualInstallUpdate = 'app:manual-install-update', App_Reload = 'app:reload', App_Quit = 'app:quit', App_Info = 'app:info', + App_GetSigningInfo = 'app:get-signing-info', App_Proxy = 'app:proxy', App_SetLaunchToTray = 'app:set-launch-to-tray', App_SetTray = 'app:set-tray', diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 5ec6b6bf64..ebadf3a753 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -502,3 +502,5 @@ export const CHERRYIN_CONFIG = { REDIRECT_URI: 'cherrystudio://oauth/callback', SCOPES: 'openid profile email offline_access balance:read usage:read tokens:read tokens:write' } + +export const APP_NAME = 'Cherry Studio' diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f38c185d96..130d63adbb 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -164,6 +164,8 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) installPath: path.dirname(app.getPath('exe')) })) + ipcMain.handle(IpcChannel.App_GetSigningInfo, () => appService.getSigningInfo()) + ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => { let proxyConfig: ProxyConfig @@ -185,6 +187,7 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) // Update ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall()) + ipcMain.handle(IpcChannel.App_ManualInstallUpdate, () => appUpdater.manualInstallUpdate()) // language ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => { diff --git a/src/main/services/AppService.ts b/src/main/services/AppService.ts index a7e1fa9535..2d473ef259 100644 --- a/src/main/services/AppService.ts +++ b/src/main/services/AppService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { isDev, isLinux, isMac, isWin } from '@main/constant' +import { spawnSync } from 'child_process' import { app } from 'electron' import fs from 'fs' import os from 'os' @@ -7,6 +8,12 @@ import path from 'path' const logger = loggerService.withContext('AppService') +export interface SigningInfo { + teamId: string | null + bundleId: string | null + authority: string | null +} + export class AppService { private static instance: AppService @@ -21,6 +28,44 @@ export class AppService { return AppService.instance } + /** + * Get macOS app signing information (team ID, bundle ID, authority) + * Returns null values for non-macOS platforms or unsigned apps + */ + public getSigningInfo(): SigningInfo { + if (!isMac) { + return { teamId: null, bundleId: null, authority: null } + } + + const exePath = app.getPath('exe') + // /path/to/App.app/Contents/MacOS/AppName -> /path/to/App.app + const appPath = exePath.replace(/\/Contents\/MacOS\/.*$/, '') + + try { + const result = spawnSync('codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf-8', timeout: 5000 }) + + if (result.error || result.status !== 0) { + logger.warn('codesign check failed', { error: result.error, status: result.status }) + return { teamId: null, bundleId: null, authority: null } + } + + const output = result.stderr || result.stdout + + const teamIdMatch = output.match(/^TeamIdentifier=(.+)$/m) + const identifierMatch = output.match(/^Identifier=(.+)$/m) + const authorityMatch = output.match(/^Authority=([^\n]+)$/m) + + return { + teamId: teamIdMatch?.[1] || null, + bundleId: identifierMatch?.[1] || null, + authority: authorityMatch?.[1] || null + } + } catch (error) { + logger.error('Failed to get signing info', error as Error) + return { teamId: null, bundleId: null, authority: null } + } + } + public async setAppLaunchOnBoot(isLaunchOnBoot: boolean): Promise { // Set login item settings for windows and mac // linux is not supported because it requires more file operations diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index e043badd9e..8ac41b2cd3 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -1,14 +1,19 @@ import { loggerService } from '@logger' -import { isWin } from '@main/constant' +import { isMac, isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' import { generateUserAgent } from '@main/utils/systemInfo' -import { FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' +import { APP_NAME, FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import type { UpdateInfo } from 'builder-util-runtime' import { CancellationToken } from 'builder-util-runtime' +import { exec, execSync } from 'child_process' +import { promisify } from 'util' + +const execAsync = promisify(exec) import { app, net } from 'electron' import type { AppUpdater as _AppUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater' import { autoUpdater } from 'electron-updater' +import fs from 'fs' import path from 'path' import semver from 'semver' @@ -17,6 +22,17 @@ import { windowService } from './WindowService' const logger = loggerService.withContext('AppUpdater') +function getCommonHeaders() { + return { + 'User-Agent': generateUserAgent(), + 'Cache-Control': 'no-cache', + 'Client-Id': configManager.getClientId(), + 'App-Name': APP_NAME, + 'App-Version': `v${app.getVersion()}`, + OS: process.platform + } +} + // Language markers constants for multi-language release notes const LANG_MARKERS = { EN_START: '', @@ -61,10 +77,7 @@ export default class AppUpdater { autoUpdater.autoInstallOnAppQuit = false autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, - 'User-Agent': generateUserAgent(), - 'X-Client-Id': configManager.getClientId(), - // no-cache - 'Cache-Control': 'no-cache' + ...getCommonHeaders() } autoUpdater.on('error', (error) => { @@ -145,11 +158,8 @@ export default class AppUpdater { logger.info(`Fetching update config from ${configUrl} (mirror: ${mirror})`) const response = await net.fetch(configUrl, { headers: { - 'User-Agent': generateUserAgent(), - Accept: 'application/json', - 'X-Client-Id': configManager.getClientId(), - // no-cache - 'Cache-Control': 'no-cache' + ...getCommonHeaders(), + Accept: 'application/json' } }) @@ -313,6 +323,118 @@ export default class AppUpdater { setImmediate(() => autoUpdater.quitAndInstall(true, true)) } + /** + * Manual install update for macOS users with old code signing + * This bypasses Squirrel.Mac which validates code signing + */ + public async manualInstallUpdate(): Promise<{ success: boolean; error?: string }> { + if (!isMac) { + return { success: false, error: 'Manual install is only supported on macOS' } + } + + // Constants + const ZIP_PATTERN = /^Cherry-Studio-\d+\.\d+\.\d+(-arm64)?\.zip$/ + const APP_NAME = 'Cherry Studio.app' + const TARGET_PATH = `/Applications/${APP_NAME}` + const cacheDir = path.join(app.getPath('home'), 'Library/Caches', 'cherrystudio-updater', 'pending') + const extractDir = path.join(app.getPath('temp'), 'cherry-studio-update') + const newAppPath = path.join(extractDir, APP_NAME) + + // Helper functions + const findUpdateZip = (): string | null => { + if (!fs.existsSync(cacheDir)) return null + const zipFile = fs.readdirSync(cacheDir).find((f) => ZIP_PATTERN.test(f)) + return zipFile ? path.join(cacheDir, zipFile) : null + } + + const extractZip = (zipPath: string): void => { + fs.rmSync(extractDir, { recursive: true, force: true }) + fs.mkdirSync(extractDir, { recursive: true }) + execSync(`unzip -o "${zipPath}" -d "${extractDir}"`) + } + + const isValidApp = (): boolean => { + return ( + fs.existsSync(newAppPath) && + fs.existsSync(path.join(newAppPath, 'Contents', 'Info.plist')) && + fs.existsSync(path.join(newAppPath, 'Contents', 'MacOS')) + ) + } + + const replaceAppWithAdminPrivileges = async (): Promise<{ success: boolean; error?: string }> => { + const language = configManager.getLanguage() + const prompt = + language === 'zh-CN' || language === 'zh-TW' + ? 'Cherry Studio 需要管理员权限来安装更新' + : 'Cherry Studio needs administrator privileges to install the update.' + + const scriptPath = path.join(app.getPath('temp'), `cherry-update-${Date.now()}.scpt`) + const scriptContent = `do shell script "rm -rf \\"${TARGET_PATH}\\" && mv \\"${newAppPath}\\" \\"${TARGET_PATH}\\"" with administrator privileges with prompt "${prompt}"` + + try { + fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf-8', mode: 0o600 }) + await execAsync(`osascript "${scriptPath}"`, { timeout: 60000 }) + logger.info('Manual install: app replaced successfully') + return { success: true } + } catch (error) { + const msg = (error as Error).message + logger.error('Manual install: replace failed', error as Error) + if (msg.includes('User canceled') || msg.includes('-128')) { + return { success: false, error: 'User cancelled' } + } + return { success: false, error: msg } + } finally { + fs.rmSync(scriptPath, { force: true }) + } + } + + const scheduleRelaunch = (): void => { + const pid = process.pid + const scriptPath = path.join(app.getPath('temp'), `cherry-relaunch-${Date.now()}.sh`) + const script = `#!/bin/sh +sleep 1 +kill -9 ${pid} 2>/dev/null +# Wait for process exit (max 30s) +for i in $(seq 1 60); do kill -0 ${pid} 2>/dev/null || break; sleep 0.5; done +open "${TARGET_PATH}" +rm -f "${scriptPath}" +` + fs.writeFileSync(scriptPath, script, { mode: 0o755 }) + + const { spawn } = require('child_process') + spawn('/bin/sh', [scriptPath], { detached: true, stdio: 'ignore' }).unref() + } + + // Main logic + const updateZip = findUpdateZip() + if (!updateZip) { + logger.error('Manual install: valid zip file not found', { cacheDir }) + return { success: false, error: 'Update file not found' } + } + + logger.info('Manual install: found update zip', { updateZip }) + + try { + extractZip(updateZip) + + if (!isValidApp()) { + logger.error('Manual install: extracted app invalid', { newAppPath }) + return { success: false, error: 'Extracted app not found' } + } + + const result = await replaceAppWithAdminPrivileges() + if (!result.success) { + return result + } + + scheduleRelaunch() + return { success: true } + } catch (error) { + logger.error('Manual install failed', error as Error) + return { success: false, error: (error as Error).message } + } + } + /** * Check if release notes contain multi-language markers */ diff --git a/src/preload/index.ts b/src/preload/index.ts index 4f8ee82b8d..42a593ce14 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -101,6 +101,8 @@ export function tracedInvoke(channel: string, spanContext: SpanContext | undefin // Custom APIs for renderer const api = { getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info), + getSigningInfo: (): Promise<{ teamId: string | null; bundleId: string | null; authority: string | null }> => + ipcRenderer.invoke(IpcChannel.App_GetSigningInfo), getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> => ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), @@ -109,6 +111,8 @@ const api = { ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate), quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall), + manualInstallUpdate: (): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke(IpcChannel.App_ManualInstallUpdate), setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang), setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable), setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages), diff --git a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx index 9d891610fa..6f6d3d98b1 100644 --- a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -1,5 +1,7 @@ +import { DownloadOutlined, InfoCircleOutlined } from '@ant-design/icons' import { loggerService } from '@logger' import { TopView } from '@renderer/components/TopView' +import { isMac } from '@renderer/config/constant' import { handleSaveData, useAppDispatch } from '@renderer/store' import { setUpdateState } from '@renderer/store/runtime' import { Button, Modal } from 'antd' @@ -11,6 +13,10 @@ import styled from 'styled-components' const logger = loggerService.withContext('UpdateDialog') +// Old Team ID that requires manual install +const OLD_TEAM_ID = 'Q24M7JR2C4' +const DOWNLOAD_URL = 'https://www.cherry-ai.com/download' + interface ShowParams { releaseInfo: UpdateInfo | null } @@ -23,12 +29,25 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { const { t } = useTranslation() const [open, setOpen] = useState(true) const [isInstalling, setIsInstalling] = useState(false) + const [requiresManualInstall, setRequiresManualInstall] = useState(false) const dispatch = useAppDispatch() useEffect(() => { if (releaseInfo) { logger.info('Update dialog opened', { version: releaseInfo.version }) } + + // Check if macOS user with old Team ID needs manual install + if (isMac) { + window.api.getSigningInfo().then((signingInfo) => { + if (signingInfo.teamId === OLD_TEAM_ID) { + setRequiresManualInstall(true) + logger.info('Manual install required', { teamId: signingInfo.teamId }) + } + }) + } + + setRequiresManualInstall(true) }, [releaseInfo]) const handleInstall = async () => { @@ -44,6 +63,32 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { } } + const handleManualInstall = async () => { + setIsInstalling(true) + try { + await handleSaveData() + const result = await window.api.manualInstallUpdate() + + if (!result.success) { + setIsInstalling(false) + if (result.error === 'User cancelled') { + // User cancelled password dialog, do nothing + return + } + logger.error('Manual install failed', { error: result.error }) + window.toast.error(t('update.manualInstallError')) + // Fallback to download page + window.api.openWebsite(DOWNLOAD_URL) + } + // If success, app will relaunch automatically + } catch (error) { + logger.error('Manual install error', error as Error) + setIsInstalling(false) + window.toast.error(t('update.manualInstallError')) + window.api.openWebsite(DOWNLOAD_URL) + } + } + const onCancel = () => { dispatch(setUpdateState({ manualCheck: false })) setOpen(false) @@ -80,11 +125,35 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { , - + requiresManualInstall ? ( + + ) : ( + + ) ]}> + {requiresManualInstall && ( +
+ + + {t('update.manualInstallInfo')} + + +
+ )} {typeof releaseNotes === 'string' diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 3e30fa2016..15c2c07dac 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -5632,8 +5632,12 @@ "show_window": "Show Window" }, "update": { + "download": "Download", "install": "Install", "later": "Later", + "manualDownload": "Download", + "manualInstallError": "Installation failed, please download manually from the official website.", + "manualInstallInfo": "This version does not support automatic updates. Click download to complete the update.", "message": "New version {{version}} is ready, do you want to install it now?", "noReleaseNotes": "No release notes", "saveDataError": "Failed to save data, please try again.", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2325868241..2c8141a639 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -5632,8 +5632,12 @@ "show_window": "显示窗口" }, "update": { + "download": "前往下载", "install": "立即安装", "later": "稍后", + "manualDownload": "下载", + "manualInstallError": "安装失败,请前往官网手动下载安装。", + "manualInstallInfo": "该版本不支持自动更新,点击下载按钮完成更新。", "message": "发现新版本 {{version}},是否立即安装?", "noReleaseNotes": "暂无更新日志", "saveDataError": "保存数据失败,请重试", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b2bdd66d10..589b884fe9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -5632,8 +5632,12 @@ "show_window": "顯示視窗" }, "update": { + "download": "前往下載", "install": "立即安裝", "later": "稍後", + "manualDownload": "下載", + "manualInstallError": "安裝失敗,請前往官網手動下載安裝。", + "manualInstallInfo": "該版本不支援自動更新,點擊下載按鈕完成更新。", "message": "新版本 {{version}} 已準備就緒,是否立即安裝?", "noReleaseNotes": "暫無更新日誌", "saveDataError": "儲存資料失敗,請重試", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index f37709f05a..8f0c959af5 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -5632,8 +5632,12 @@ "show_window": "Fenster anzeigen" }, "update": { + "download": "Herunterladen", "install": "Jetzt installieren", "later": "Später", + "manualDownload": "Herunterladen", + "manualInstallError": "Installation fehlgeschlagen, bitte manuell von der offiziellen Website herunterladen.", + "manualInstallInfo": "Diese Version unterstützt keine automatischen Updates. Klicken Sie auf Herunterladen, um das Update abzuschließen.", "message": "Neue Version {{version}} gefunden. Jetzt installieren?", "noReleaseNotes": "Kein Changelog verfügbar", "saveDataError": "Speichern fehlgeschlagen, bitte erneut versuchen", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 6164f410bb..f5b65fe718 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -5632,8 +5632,12 @@ "show_window": "Εμφάνιση παραθύρου" }, "update": { + "download": "Λήψη", "install": "Εγκατάσταση", "later": "Μετά", + "manualDownload": "Λήψη", + "manualInstallError": "Η εγκατάσταση απέτυχε, παρακαλώ κατεβάστε χειροκίνητα από την επίσημη ιστοσελίδα.", + "manualInstallInfo": "Αυτή η έκδοση δεν υποστηρίζει αυτόματες ενημερώσεις. Κάντε κλικ στη λήψη για να ολοκληρώσετε την ενημέρωση.", "message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;", "noReleaseNotes": "Χωρίς σημειώσεις", "saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 4ab09360db..5b6ef496b3 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -5632,8 +5632,12 @@ "show_window": "Mostrar ventana" }, "update": { + "download": "Descargar", "install": "Instalar", "later": "Más tarde", + "manualDownload": "Descargar", + "manualInstallError": "La instalación falló, por favor descarga manualmente desde el sitio web oficial.", + "manualInstallInfo": "Esta versión no admite actualizaciones automáticas. Haz clic en descargar para completar la actualización.", "message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?", "noReleaseNotes": "Sin notas de la versión", "saveDataError": "Error al guardar los datos, inténtalo de nuevo", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 0ee5aa549f..d2c3ca4e28 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -5632,8 +5632,12 @@ "show_window": "Afficher la fenêtre" }, "update": { + "download": "Télécharger", "install": "Installer", "later": "Plus tard", + "manualDownload": "Télécharger", + "manualInstallError": "L'installation a échoué, veuillez télécharger manuellement depuis le site officiel.", + "manualInstallInfo": "Cette version ne prend pas en charge les mises à jour automatiques. Cliquez sur télécharger pour terminer la mise à jour.", "message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?", "noReleaseNotes": "Aucune note de version", "saveDataError": "Échec de la sauvegarde des données, veuillez réessayer", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 58c92a0402..fd9d6c2a29 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -5632,8 +5632,12 @@ "show_window": "ウィンドウを表示" }, "update": { + "download": "ダウンロード", "install": "今すぐインストール", "later": "後で", + "manualDownload": "ダウンロード", + "manualInstallError": "インストールに失敗しました。公式ウェブサイトから手動でダウンロードしてください。", + "manualInstallInfo": "このバージョンは自動更新に対応していません。ダウンロードをクリックして更新を完了してください。", "message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?", "noReleaseNotes": "暫無更新日誌", "saveDataError": "データの保存に失敗しました。もう一度お試しください。", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 83ac9812ee..fe1aff0448 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -5632,8 +5632,12 @@ "show_window": "Exibir Janela" }, "update": { + "download": "Baixar", "install": "Instalar", "later": "Mais tarde", + "manualDownload": "Baixar", + "manualInstallError": "A instalação falhou, por favor, faça o download manualmente no site oficial.", + "manualInstallInfo": "Esta versão não suporta atualizações automáticas. Clique em baixar para concluir a atualização.", "message": "Nova versão {{version}} disponível, deseja instalar agora?", "noReleaseNotes": "Sem notas de versão", "saveDataError": "Falha ao salvar os dados, tente novamente", diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index b275f72111..fe71ab16d3 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -5632,8 +5632,12 @@ "show_window": "Afișează fereastra" }, "update": { + "download": "Descărcare", "install": "Instalează", "later": "Mai târziu", + "manualDownload": "Descărcați", + "manualInstallError": "Instalarea a eșuat, vă rugăm să descărcați manual de pe site-ul oficial.", + "manualInstallInfo": "Această versiune nu suportă actualizări automate. Faceți clic pe descărcare pentru a finaliza actualizarea.", "message": "Noua versiune {{version}} este gata, vrei să o instalezi acum?", "noReleaseNotes": "Nicio notă de lansare", "saveDataError": "Salvarea datelor a eșuat, te rugăm să încerci din nou.", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 6db14eb211..871bad0407 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -5632,8 +5632,12 @@ "show_window": "Показать окно" }, "update": { + "download": "Скачать", "install": "Установить", "later": "Позже", + "manualDownload": "Скачать", + "manualInstallError": "Установка не удалась, пожалуйста, загрузите вручную с официального сайта.", + "manualInstallInfo": "Эта версия не поддерживает автоматическое обновление. Нажмите «Скачать», чтобы завершить обновление.", "message": "Новая версия {{version}} готова, установить сейчас?", "noReleaseNotes": "Нет заметок об обновлении", "saveDataError": "Ошибка сохранения данных, повторите попытку",