mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 15:25:27 +08:00
Compare commits
4 Commits
feat/provi
...
fix/shell-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ce5b8313 | ||
|
|
f6326c8082 | ||
|
|
c5fe127958 | ||
|
|
9a33426eed |
@@ -76,6 +76,19 @@ const __dirname = getModuleDir(import.meta.url);
|
|||||||
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
||||||
const APP_ROOT = findAppRoot(__dirname);
|
const APP_ROOT = findAppRoot(__dirname);
|
||||||
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
||||||
|
// Version of the code that is actually running, captured once at process
|
||||||
|
// startup. This intentionally does NOT re-read package.json per request: after
|
||||||
|
// an update replaces the files on disk, package.json reflects the NEW version
|
||||||
|
// while this long-lived process still runs the OLD code. The frontend bundle is
|
||||||
|
// rebuilt on update, so a mismatch between this value and the frontend's
|
||||||
|
// build-time version means the server was updated but not restarted.
|
||||||
|
const RUNNING_VERSION = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(path.join(APP_ROOT, 'package.json'), 'utf8')).version || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
const MAX_FILE_UPLOAD_SIZE_MB = 200;
|
const MAX_FILE_UPLOAD_SIZE_MB = 200;
|
||||||
const MAX_FILE_UPLOAD_SIZE_BYTES = MAX_FILE_UPLOAD_SIZE_MB * 1024 * 1024;
|
const MAX_FILE_UPLOAD_SIZE_BYTES = MAX_FILE_UPLOAD_SIZE_MB * 1024 * 1024;
|
||||||
const MAX_FILE_UPLOAD_COUNT = 20;
|
const MAX_FILE_UPLOAD_COUNT = 20;
|
||||||
@@ -156,7 +169,8 @@ app.get('/health', (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
installMode
|
installMode,
|
||||||
|
version: RUNNING_VERSION
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,62 @@ function buildShellCommand(
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
|
||||||
|
const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase());
|
||||||
|
return resolvedKey ? env[resolvedKey] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathEnvKey(env: NodeJS.ProcessEnv): string {
|
||||||
|
return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
||||||
|
}
|
||||||
|
|
||||||
|
function prioritizeUserNpmGlobalBin(env: NodeJS.ProcessEnv): { key: string; value: string | undefined } {
|
||||||
|
const pathKey = getPathEnvKey(env);
|
||||||
|
const currentPath = env[pathKey];
|
||||||
|
if (!currentPath) {
|
||||||
|
return { key: pathKey, value: currentPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
const delimiter = path.delimiter;
|
||||||
|
const pathEntries = currentPath.split(delimiter).filter(Boolean);
|
||||||
|
const npmPrefix = readEnvValue(env, 'npm_config_prefix');
|
||||||
|
const appData = readEnvValue(env, 'APPDATA');
|
||||||
|
const candidates = [
|
||||||
|
npmPrefix || '',
|
||||||
|
npmPrefix ? path.join(npmPrefix, 'bin') : '',
|
||||||
|
appData ? path.join(appData, 'npm') : '',
|
||||||
|
path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
|
||||||
|
path.join(os.homedir(), '.npm-global', 'bin'),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry);
|
||||||
|
const preferredEntries = candidates.filter((candidate, index) => {
|
||||||
|
const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate;
|
||||||
|
return (
|
||||||
|
candidates.indexOf(candidate) === index &&
|
||||||
|
normalizedPathEntries.includes(normalizedCandidate)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (preferredEntries.length === 0) {
|
||||||
|
return { key: pathKey, value: currentPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPreferredEntries = preferredEntries.map((entry) =>
|
||||||
|
os.platform() === 'win32' ? entry.toLowerCase() : entry
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = [
|
||||||
|
...preferredEntries,
|
||||||
|
...pathEntries.filter((entry) => {
|
||||||
|
const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry;
|
||||||
|
return !normalizedPreferredEntries.includes(normalizedEntry);
|
||||||
|
}),
|
||||||
|
].join(delimiter);
|
||||||
|
|
||||||
|
return { key: pathKey, value };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles websocket connections used by the standalone shell terminal UI.
|
* Handles websocket connections used by the standalone shell terminal UI.
|
||||||
*/
|
*/
|
||||||
@@ -284,6 +340,7 @@ export function handleShellConnection(
|
|||||||
os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
||||||
const termCols = readNumber(data.cols, 80);
|
const termCols = readNumber(data.cols, 80);
|
||||||
const termRows = readNumber(data.rows, 24);
|
const termRows = readNumber(data.rows, 24);
|
||||||
|
const prioritizedPath = prioritizeUserNpmGlobalBin(process.env);
|
||||||
|
|
||||||
shellProcess = pty.spawn(shell, shellArgs, {
|
shellProcess = pty.spawn(shell, shellArgs, {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
@@ -292,6 +349,7 @@ export function handleShellConnection(
|
|||||||
cwd: resolvedProjectPath,
|
cwd: resolvedProjectPath,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
[prioritizedPath.key]: prioritizedPath.value,
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
COLORTERM: 'truecolor',
|
COLORTERM: 'truecolor',
|
||||||
FORCE_COLOR: '3',
|
FORCE_COLOR: '3',
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function Sidebar({
|
|||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const { t } = useTranslation(['sidebar', 'common']);
|
const { t } = useTranslation(['sidebar', 'common']);
|
||||||
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
||||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
|
const { updateAvailable, restartRequired, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
|
||||||
'siteboon',
|
'siteboon',
|
||||||
'claudecodeui',
|
'claudecodeui',
|
||||||
);
|
);
|
||||||
@@ -224,6 +224,7 @@ function Sidebar({
|
|||||||
onExpand={handleExpandSidebar}
|
onExpand={handleExpandSidebar}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
|
restartRequired={restartRequired}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -296,6 +297,7 @@ function Sidebar({
|
|||||||
onCreateProject={() => setShowNewProject(true)}
|
onCreateProject={() => setShowNewProject(true)}
|
||||||
onCollapseSidebar={handleCollapseSidebar}
|
onCollapseSidebar={handleCollapseSidebar}
|
||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
|
restartRequired={restartRequired}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Settings, Sparkles, PanelLeftOpen, Bug } from 'lucide-react';
|
import { Settings, Sparkles, PanelLeftOpen, Bug, AlertTriangle } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
|
||||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
@@ -16,6 +16,7 @@ type SidebarCollapsedProps = {
|
|||||||
onExpand: () => void;
|
onExpand: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
|
restartRequired: boolean;
|
||||||
onShowVersionModal: () => void;
|
onShowVersionModal: () => void;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
};
|
};
|
||||||
@@ -24,6 +25,7 @@ export default function SidebarCollapsed({
|
|||||||
onExpand,
|
onExpand,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
|
restartRequired,
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
t,
|
t,
|
||||||
}: SidebarCollapsedProps) {
|
}: SidebarCollapsedProps) {
|
||||||
@@ -75,6 +77,18 @@ export default function SidebarCollapsed({
|
|||||||
<DiscordIcon className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
<DiscordIcon className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{/* Restart-required indicator */}
|
||||||
|
{restartRequired && (
|
||||||
|
<div
|
||||||
|
className="relative flex h-8 w-8 items-center justify-center rounded-lg"
|
||||||
|
aria-label={t('version.restartRequired')}
|
||||||
|
title={t('version.restartRequired')}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||||
|
<span className="absolute right-1.5 top-1.5 h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Update indicator */}
|
{/* Update indicator */}
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ type SidebarContentProps = {
|
|||||||
onCreateProject: () => void;
|
onCreateProject: () => void;
|
||||||
onCollapseSidebar: () => void;
|
onCollapseSidebar: () => void;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
|
restartRequired: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
@@ -178,6 +179,7 @@ export default function SidebarContent({
|
|||||||
onCreateProject,
|
onCreateProject,
|
||||||
onCollapseSidebar,
|
onCollapseSidebar,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
|
restartRequired,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
@@ -553,6 +555,7 @@ export default function SidebarContent({
|
|||||||
|
|
||||||
<SidebarFooter
|
<SidebarFooter
|
||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
|
restartRequired={restartRequired}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Settings, ArrowUpCircle, Bug } from 'lucide-react';
|
import { Settings, ArrowUpCircle, Bug, AlertTriangle } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
@@ -18,6 +18,7 @@ function DiscordIcon({ className }: { className?: string }) {
|
|||||||
|
|
||||||
type SidebarFooterProps = {
|
type SidebarFooterProps = {
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
|
restartRequired: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
@@ -28,6 +29,7 @@ type SidebarFooterProps = {
|
|||||||
|
|
||||||
export default function SidebarFooter({
|
export default function SidebarFooter({
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
|
restartRequired,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
@@ -37,6 +39,22 @@ export default function SidebarFooter({
|
|||||||
}: SidebarFooterProps) {
|
}: SidebarFooterProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0)' }}>
|
<div className="flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0)' }}>
|
||||||
|
{/* Restart-required banner: the running server version differs from the
|
||||||
|
installed/frontend version (updated but not restarted). */}
|
||||||
|
{restartRequired && (
|
||||||
|
<>
|
||||||
|
<div className="nav-divider" />
|
||||||
|
<div className="px-2 py-1.5 md:px-2 md:py-1.5">
|
||||||
|
<div className="flex items-center gap-2.5 rounded-lg border border-amber-300/60 bg-amber-50/80 px-2.5 py-2 dark:border-amber-700/40 dark:bg-amber-900/15">
|
||||||
|
<AlertTriangle className="h-4 w-4 flex-shrink-0 text-amber-500 dark:text-amber-400" />
|
||||||
|
<span className="min-w-0 flex-1 text-xs font-medium text-amber-700 dark:text-amber-300">
|
||||||
|
{t('version.restartRequired')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Update banner */}
|
{/* Update banner */}
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -28,20 +28,31 @@ export const useVersionCheck = (owner: string, repo: string) => {
|
|||||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||||
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
|
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
|
||||||
const [installMode, setInstallMode] = useState<InstallMode>('git');
|
const [installMode, setInstallMode] = useState<InstallMode>('git');
|
||||||
|
const [runningVersion, setRunningVersion] = useState<string | null>(null);
|
||||||
|
const [restartRequired, setRestartRequired] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInstallMode = async () => {
|
const fetchHealth = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/health');
|
const response = await fetch('/health');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.installMode === 'npm' || data.installMode === 'git') {
|
if (data.installMode === 'npm' || data.installMode === 'git') {
|
||||||
setInstallMode(data.installMode);
|
setInstallMode(data.installMode);
|
||||||
}
|
}
|
||||||
|
// `data.version` is the version the server process is actually running.
|
||||||
|
// This module's `version` is baked into the frontend bundle at build
|
||||||
|
// time, so it reflects the installed (on-disk) package. If they differ,
|
||||||
|
// the package was updated but the server process was not restarted, and
|
||||||
|
// DB-backed actions may silently fail until it is.
|
||||||
|
if (typeof data.version === 'string' && data.version.length > 0) {
|
||||||
|
setRunningVersion(data.version);
|
||||||
|
setRestartRequired(data.version !== version);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Default to git on error
|
// Default to git / no restart hint on error
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchInstallMode();
|
fetchHealth();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -84,5 +95,5 @@ export const useVersionCheck = (owner: string, repo: string) => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [owner, repo]);
|
}, [owner, repo]);
|
||||||
|
|
||||||
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo, installMode };
|
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo, installMode, runningVersion, restartRequired };
|
||||||
};
|
};
|
||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Fehler beim Wiederherstellen der Sitzung. Bitte erneut versuchen."
|
"restoreSessionError": "Fehler beim Wiederherstellen der Sitzung. Bitte erneut versuchen."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Update verfügbar"
|
"updateAvailable": "Update verfügbar",
|
||||||
|
"restartRequired": "Update installiert – zum Anwenden Server neu starten"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Projekte",
|
"modeProjects": "Projekte",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Error restoring session. Please try again."
|
"restoreSessionError": "Error restoring session. Please try again."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Update available"
|
"updateAvailable": "Update available",
|
||||||
|
"restartRequired": "Update installed — restart the server to apply"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Projects",
|
"modeProjects": "Projects",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Erreur lors de la restauration de la session. Veuillez réessayer."
|
"restoreSessionError": "Erreur lors de la restauration de la session. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Mise à jour disponible"
|
"updateAvailable": "Mise à jour disponible",
|
||||||
|
"restartRequired": "Mise à jour installée — redémarrez le serveur pour l'appliquer"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Projets",
|
"modeProjects": "Projets",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Errore durante il ripristino della sessione. Riprova."
|
"restoreSessionError": "Errore durante il ripristino della sessione. Riprova."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Aggiornamento disponibile"
|
"updateAvailable": "Aggiornamento disponibile",
|
||||||
|
"restartRequired": "Aggiornamento installato — riavvia il server per applicarlo"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Progetti",
|
"modeProjects": "Progetti",
|
||||||
|
|||||||
@@ -114,7 +114,8 @@
|
|||||||
"restoreSessionError": "セッションの復元でエラーが発生しました。もう一度お試しください。"
|
"restoreSessionError": "セッションの復元でエラーが発生しました。もう一度お試しください。"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "アップデートあり"
|
"updateAvailable": "アップデートあり",
|
||||||
|
"restartRequired": "更新が適用されていません。サーバーを再起動してください"
|
||||||
},
|
},
|
||||||
"deleteConfirmation": {
|
"deleteConfirmation": {
|
||||||
"deleteProject": "プロジェクトを除去",
|
"deleteProject": "プロジェクトを除去",
|
||||||
|
|||||||
@@ -114,7 +114,8 @@
|
|||||||
"restoreSessionError": "세션 복원 오류. 다시 시도해주세요."
|
"restoreSessionError": "세션 복원 오류. 다시 시도해주세요."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "업데이트 가능"
|
"updateAvailable": "업데이트 가능",
|
||||||
|
"restartRequired": "업데이트가 설치됨 — 적용하려면 서버를 재시작하세요"
|
||||||
},
|
},
|
||||||
"deleteConfirmation": {
|
"deleteConfirmation": {
|
||||||
"deleteProject": "프로젝트 제거",
|
"deleteProject": "프로젝트 제거",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Ошибка при восстановлении сеанса. Попробуйте снова."
|
"restoreSessionError": "Ошибка при восстановлении сеанса. Попробуйте снова."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Доступно обновление"
|
"updateAvailable": "Доступно обновление",
|
||||||
|
"restartRequired": "Обновление установлено — перезапустите сервер для применения"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Проекты",
|
"modeProjects": "Проекты",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "Oturum geri yüklenirken hata oluştu. Lütfen tekrar dene."
|
"restoreSessionError": "Oturum geri yüklenirken hata oluştu. Lütfen tekrar dene."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Güncelleme mevcut"
|
"updateAvailable": "Güncelleme mevcut",
|
||||||
|
"restartRequired": "Güncelleme yüklendi — uygulamak için sunucuyu yeniden başlatın"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "Projeler",
|
"modeProjects": "Projeler",
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"restoreSessionError": "恢复会话时出错,请重试。"
|
"restoreSessionError": "恢复会话时出错,请重试。"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "有可用更新"
|
"updateAvailable": "有可用更新",
|
||||||
|
"restartRequired": "已安装更新 — 请重启服务器以生效"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "项目",
|
"modeProjects": "项目",
|
||||||
|
|||||||
@@ -114,7 +114,8 @@
|
|||||||
"restoreSessionError": "還原工作階段時出錯,請重試。"
|
"restoreSessionError": "還原工作階段時出錯,請重試。"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "有可用更新"
|
"updateAvailable": "有可用更新",
|
||||||
|
"restartRequired": "已安裝更新 — 請重新啟動伺服器以套用"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"modeProjects": "專案",
|
"modeProjects": "專案",
|
||||||
|
|||||||
Reference in New Issue
Block a user