diff --git a/biome.jsonc b/biome.jsonc index 1568fec7286..0a7e4b03c1c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -14,7 +14,7 @@ } }, "enabled": true, - "includes": ["**/*.json", "!*.json", "!**/package.json", "!coverage/**"] + "includes": ["**/*.json", "!*.json", "!**/package.json", "!coverage/**", "!config/app-upgrade-segments.json"] }, "css": { "formatter": { diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json index 70c8ac25f0a..d0148b263b4 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.7.25" }, + "lockedVersion": "1.7.25", "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.7.25 - locked to v1.7.25", "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.7.25 <2.0.0" + }, + "minCompatibleVersion": "1.7.25", + "description": "Current latest v1.x release for users on v1.7.25+", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" } }, "rc": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 85d8fd9261d..12444832c4b 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 75bb853f57d..47b681bf9e4 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 587c7393a21..1d3ae10215c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -166,6 +166,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 @@ -187,6 +189,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 a7e1fa95351..2d473ef2596 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 57dc3fb2a83..e18bc52e325 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: '', @@ -58,10 +74,7 @@ export default class AppUpdater { autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate() autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, - 'User-Agent': generateUserAgent(), - 'X-Client-Id': configManager.getClientId(), - // no-cache - 'Cache-Control': 'no-cache' + ...getCommonHeaders() } autoUpdater.on('error', (error) => { @@ -142,11 +155,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' } }) @@ -310,6 +320,118 @@ export default class AppUpdater { setImmediate(() => autoUpdater.quitAndInstall()) } + /** + * 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 ba90d273683..f5983ecd856 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -103,6 +103,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), @@ -111,6 +113,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 593c882bf51..14930f9bcb3 100644 --- a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -1,8 +1,9 @@ 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' +import { Alert, Button, Modal } from 'antd' import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -11,6 +12,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 +28,23 @@ 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 }) + } + }) + } }, [releaseInfo]) const handleInstall = async () => { @@ -44,6 +60,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 = () => { setOpen(false) } @@ -79,11 +121,20 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { , - + requiresManualInstall ? ( + + ) : ( + + ) ]}> + {requiresManualInstall && ( + + )} {typeof releaseNotes === 'string' diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 3c8bb80a947..f4d5d84fa46 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -5556,8 +5556,11 @@ "show_window": "Show Window" }, "update": { + "download": "Download", "install": "Install", "later": "Later", + "manualInstallError": "Installation failed, please download manually from the official website.", + "manualInstallInfo": "Click install and the system will guide you through 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 36d90127dd1..7e84bd8eb34 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -5556,8 +5556,11 @@ "show_window": "显示窗口" }, "update": { + "download": "前往下载", "install": "立即安装", "later": "稍后", + "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 daf44c07540..1bcfbb8d801 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -5556,8 +5556,11 @@ "show_window": "顯示視窗" }, "update": { + "download": "前往下載", "install": "立即安裝", "later": "稍後", + "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 705fe271d1a..b835ebc2461 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -5556,8 +5556,11 @@ "show_window": "Fenster anzeigen" }, "update": { + "download": "Herunterladen", "install": "Jetzt installieren", "later": "Später", + "manualInstallError": "Installation fehlgeschlagen, bitte manuell von der offiziellen Website herunterladen.", + "manualInstallInfo": "Klicken Sie auf Installieren und das System führt Sie durch das Update.", "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 befdd6ba4b3..83c3e5cc29a 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -5556,8 +5556,11 @@ "show_window": "Εμφάνιση παραθύρου" }, "update": { + "download": "Λήψη", "install": "Εγκατάσταση", "later": "Μετά", + "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 a90f760b8e8..698fa238b93 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -5556,8 +5556,11 @@ "show_window": "Mostrar ventana" }, "update": { + "download": "Descargar", "install": "Instalar", "later": "Más tarde", + "manualInstallError": "La instalación falló, por favor descarga manualmente desde el sitio web oficial.", + "manualInstallInfo": "Haz clic en instalar y el sistema te guiará a través de 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 b3c3bd6ba74..a5808509124 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -5556,8 +5556,11 @@ "show_window": "Afficher la fenêtre" }, "update": { + "download": "Télécharger", "install": "Installer", "later": "Plus tard", + "manualInstallError": "L'installation a échoué, veuillez télécharger manuellement depuis le site officiel.", + "manualInstallInfo": "Cliquez sur installer et le système vous guidera à travers 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 85b8de18821..36410792c76 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -5556,8 +5556,11 @@ "show_window": "ウィンドウを表示" }, "update": { + "download": "ダウンロード", "install": "今すぐインストール", "later": "後で", + "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 3e2e0dd46c0..6c3a23bd858 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -5556,8 +5556,11 @@ "show_window": "Exibir Janela" }, "update": { + "download": "Baixar", "install": "Instalar", "later": "Mais tarde", + "manualInstallError": "A instalação falhou, por favor, faça o download manualmente no site oficial.", + "manualInstallInfo": "Clique em instalar e o sistema irá guiá-lo através da 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 7eb3065811d..dec56dcf59a 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -5556,8 +5556,11 @@ "show_window": "Afișează fereastra" }, "update": { + "download": "Descărcare", "install": "Instalează", "later": "Mai târziu", + "manualInstallError": "Instalarea a eșuat, vă rugăm să descărcați manual de pe site-ul oficial.", + "manualInstallInfo": "Faceți clic pe instalare și sistemul vă va ghida prin actualizare.", "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 c5e2c98a3f6..05a5287f2ce 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -5556,8 +5556,11 @@ "show_window": "Показать окно" }, "update": { + "download": "Скачать", "install": "Установить", "later": "Позже", + "manualInstallError": "Установка не удалась, пожалуйста, загрузите вручную с официального сайта.", + "manualInstallInfo": "Нажмите «Установить», и система проведёт вас через обновление.", "message": "Новая версия {{version}} готова, установить сейчас?", "noReleaseNotes": "Нет заметок об обновлении", "saveDataError": "Ошибка сохранения данных, повторите попытку",