mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
refactor: remove manual install update logic and related API calls
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**", "!config/app-upgrade-segments.json"],
|
||||
"includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
@@ -7,11 +7,9 @@ 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',
|
||||
|
||||
@@ -164,8 +164,6 @@ 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,7 +185,6 @@ 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) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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'
|
||||
@@ -8,12 +7,6 @@ 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
|
||||
|
||||
@@ -28,44 +21,6 @@ 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<void> {
|
||||
// Set login item settings for windows and mac
|
||||
// linux is not supported because it requires more file operations
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { isWin } from '@main/constant'
|
||||
import { getIpCountry } from '@main/utils/ipService'
|
||||
import { generateUserAgent } from '@main/utils/systemInfo'
|
||||
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'
|
||||
|
||||
@@ -323,118 +318,6 @@ 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
|
||||
*/
|
||||
|
||||
@@ -101,8 +101,6 @@ 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,8 +109,6 @@ 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),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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'
|
||||
@@ -13,10 +11,6 @@ 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
|
||||
}
|
||||
@@ -29,25 +23,12 @@ const PopupContainer: React.FC<Props> = ({ 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 () => {
|
||||
@@ -63,32 +44,6 @@ const PopupContainer: React.FC<Props> = ({ 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)
|
||||
@@ -125,35 +80,11 @@ const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
|
||||
<Button key="later" onClick={onIgnore} disabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>,
|
||||
requiresManualInstall ? (
|
||||
<Button key="install" type="primary" onClick={handleManualInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
)
|
||||
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
]}>
|
||||
<ModalBodyWrapper>
|
||||
{requiresManualInstall && (
|
||||
<div className="mb-4 flex items-center gap-3 rounded-lg border border-neutral-200 bg-neutral-100 px-3 py-2 dark:border-neutral-700 dark:bg-neutral-800/50">
|
||||
<InfoCircleOutlined className="shrink-0 text-base text-neutral-500 dark:text-neutral-400" />
|
||||
<span className="flex-1 text-neutral-600 text-sm dark:text-neutral-300">
|
||||
{t('update.manualInstallInfo')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.api.openWebsite(DOWNLOAD_URL)}
|
||||
className="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-3 py-1 font-medium text-sm text-white transition-colors"
|
||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.85')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}>
|
||||
<DownloadOutlined className="text-xs" />
|
||||
{t('update.manualDownload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ReleaseNotesWrapper className="markdown">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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.",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"show_window": "显示窗口"
|
||||
},
|
||||
"update": {
|
||||
"download": "前往下载",
|
||||
"install": "立即安装",
|
||||
"later": "稍后",
|
||||
"manualDownload": "下载",
|
||||
"manualInstallError": "安装失败,请前往官网手动下载安装。",
|
||||
"manualInstallInfo": "该版本不支持自动更新,点击下载按钮完成更新。",
|
||||
"message": "发现新版本 {{version}},是否立即安装?",
|
||||
"noReleaseNotes": "暂无更新日志",
|
||||
"saveDataError": "保存数据失败,请重试",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"show_window": "顯示視窗"
|
||||
},
|
||||
"update": {
|
||||
"download": "前往下載",
|
||||
"install": "立即安裝",
|
||||
"later": "稍後",
|
||||
"manualDownload": "下載",
|
||||
"manualInstallError": "安裝失敗,請前往官網手動下載安裝。",
|
||||
"manualInstallInfo": "該版本不支援自動更新,點擊下載按鈕完成更新。",
|
||||
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
|
||||
"noReleaseNotes": "暫無更新日誌",
|
||||
"saveDataError": "儲存資料失敗,請重試",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"show_window": "Εμφάνιση παραθύρου"
|
||||
},
|
||||
"update": {
|
||||
"download": "Λήψη",
|
||||
"install": "Εγκατάσταση",
|
||||
"later": "Μετά",
|
||||
"manualDownload": "Λήψη",
|
||||
"manualInstallError": "Η εγκατάσταση απέτυχε, παρακαλώ κατεβάστε χειροκίνητα από την επίσημη ιστοσελίδα.",
|
||||
"manualInstallInfo": "Αυτή η έκδοση δεν υποστηρίζει αυτόματες ενημερώσεις. Κάντε κλικ στη λήψη για να ολοκληρώσετε την ενημέρωση.",
|
||||
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
|
||||
"noReleaseNotes": "Χωρίς σημειώσεις",
|
||||
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"show_window": "ウィンドウを表示"
|
||||
},
|
||||
"update": {
|
||||
"download": "ダウンロード",
|
||||
"install": "今すぐインストール",
|
||||
"later": "後で",
|
||||
"manualDownload": "ダウンロード",
|
||||
"manualInstallError": "インストールに失敗しました。公式ウェブサイトから手動でダウンロードしてください。",
|
||||
"manualInstallInfo": "このバージョンは自動更新に対応していません。ダウンロードをクリックして更新を完了してください。",
|
||||
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
|
||||
"noReleaseNotes": "暫無更新日誌",
|
||||
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"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.",
|
||||
|
||||
@@ -5632,12 +5632,8 @@
|
||||
"show_window": "Показать окно"
|
||||
},
|
||||
"update": {
|
||||
"download": "Скачать",
|
||||
"install": "Установить",
|
||||
"later": "Позже",
|
||||
"manualDownload": "Скачать",
|
||||
"manualInstallError": "Установка не удалась, пожалуйста, загрузите вручную с официального сайта.",
|
||||
"manualInstallInfo": "Эта версия не поддерживает автоматическое обновление. Нажмите «Скачать», чтобы завершить обновление.",
|
||||
"message": "Новая версия {{version}} готова, установить сейчас?",
|
||||
"noReleaseNotes": "Нет заметок об обновлении",
|
||||
"saveDataError": "Ошибка сохранения данных, повторите попытку",
|
||||
|
||||
Reference in New Issue
Block a user