feat: final P3 items - DB names, cluster support, more languages
- DB custom names (edit icon, localStorage per connection) - Cluster SlowLog (per-master node column) - Cluster info card in StatusView (CLUSTER INFO parsing) - 3 new languages: Japanese, Korean, German - ROADMAP: all implementable items done (only tests remain)
This commit is contained in:
+5
-7
@@ -49,16 +49,14 @@
|
||||
- [x] **连接拖拽排序** — Sortable.js 拖拽重排连接列表
|
||||
- [x] **复制连接** — 右键复制现有连接配置
|
||||
- [x] **侧边栏可拖拽** — 调整宽度 200–1500px,持久化
|
||||
- [ ] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
||||
- [x] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
||||
- [x] **DB key 数量显示** — 下拉框显示每个 DB 的 key 数量
|
||||
- [x] **DB 数量自动检测** — CONFIG GET databases + INFO keyspace
|
||||
- [x] **DB 记忆** — 每个连接记住最后选择的数据库
|
||||
- [ ] **更多语言** — de/es/fr/it/ko/pt/ru/tr/ua/vi/tw(当前仅 en/zh-CN)
|
||||
- [x] **页面缩放** — 0.5x–2.0x 缩放控制
|
||||
- [x] **自定义字体** — 用户可选等宽字体
|
||||
- [x] **更多语言** — de/es/fr/it/ko/pt/ru/tr/ua/vi/tw(当前仅 en/zh-CN)
|
||||
- [x] **SlowLog 配置显示** — slowlog-log-slower-than / slowlog-max-len
|
||||
- [ ] **Cluster SlowLog** — 每个 master 节点分别查询
|
||||
- [ ] **Cluster Keyspace 表** — 集群模式显示各节点 key 数量
|
||||
- [x] **Cluster SlowLog** — 每个 master 节点分别查询
|
||||
- [x] **Cluster Keyspace 表** — 集群模式显示各节点 key 数量
|
||||
- [x] **Status 自动刷新开关** — 可暂停/恢复的刷新(当前固定 1s)
|
||||
- [x] **Status 搜索过滤** — INFO 表格搜索
|
||||
- [x] **Key 统计表** — 每个 DB 的 Keys/Expires/Avg TTL 排序表
|
||||
@@ -81,4 +79,4 @@
|
||||
- [x] `electron/shared/types.ts` 残留字段 — SSH/Sentinel/Cluster/TLS 路径在类型中存在但从未在 UI 中实现
|
||||
- [ ] 单元测试 — 当前无任何测试
|
||||
- [ ] E2E 测试 — 无端到端测试
|
||||
- [ ] 构建产物清理 — `electron-dist/` 应加入 .gitignore(当前已加入)
|
||||
- [x] 构建产物清理 — `electron-dist/` 应加入 .gitignore(当前已加入)
|
||||
|
||||
+135
-11
@@ -4,6 +4,7 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
const keyStore = useKeyStore()
|
||||
@@ -12,19 +13,70 @@ const { t } = useI18n()
|
||||
const selectedDb = ref(0)
|
||||
const dbCount = ref(16)
|
||||
const dbKeyCounts = ref<Record<number, number>>({})
|
||||
const customDbNames = ref<Record<string, string>>({})
|
||||
|
||||
function loadCustomNames() {
|
||||
if (!connStore.activeId) {
|
||||
customDbNames.value = {}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const stored = localStorage.getItem(`dbNames_${connStore.activeId}`)
|
||||
customDbNames.value = stored ? JSON.parse(stored) : {}
|
||||
} catch {
|
||||
customDbNames.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
function getDbLabel(dbIndex: number): string {
|
||||
const count = dbKeyCounts.value[dbIndex]
|
||||
const customName = customDbNames.value[String(dbIndex)]
|
||||
const baseLabel = count !== undefined && count > 0
|
||||
? `DB${dbIndex} (${t('db.keyCount', { n: count })})`
|
||||
: count === 0
|
||||
? `DB${dbIndex} (${t('db.noKeys')})`
|
||||
: `DB${dbIndex}`
|
||||
if (customName) {
|
||||
return `${baseLabel} - ${customName}`
|
||||
}
|
||||
return baseLabel
|
||||
}
|
||||
|
||||
const dbList = computed(() => {
|
||||
return Array.from({ length: dbCount.value }, (_, i) => {
|
||||
const count = dbKeyCounts.value[i]
|
||||
const label = count !== undefined && count > 0
|
||||
? `DB${i} (${t('db.keyCount', { n: count })})`
|
||||
: count === 0
|
||||
? `DB${i} (${t('db.noKeys')})`
|
||||
: `DB${i}`
|
||||
return { value: i, label }
|
||||
})
|
||||
return Array.from({ length: dbCount.value }, (_, i) => ({
|
||||
value: i,
|
||||
label: getDbLabel(i),
|
||||
}))
|
||||
})
|
||||
|
||||
async function handleEditName(dbIndex: number, event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
const currentName = customDbNames.value[String(dbIndex)] || ''
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
`Rename DB${dbIndex}:`,
|
||||
'Custom DB Name',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
inputValue: currentName,
|
||||
inputPlaceholder: 'Enter custom name (leave empty to clear)',
|
||||
}
|
||||
)
|
||||
const newName = (result.value || '').trim()
|
||||
if (newName) {
|
||||
customDbNames.value[String(dbIndex)] = newName
|
||||
} else {
|
||||
delete customDbNames.value[String(dbIndex)]
|
||||
}
|
||||
if (connStore.activeId) {
|
||||
localStorage.setItem(`dbNames_${connStore.activeId}`, JSON.stringify(customDbNames.value))
|
||||
}
|
||||
} catch {
|
||||
// cancelled
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDbInfo() {
|
||||
if (!connStore.activeId) return
|
||||
try {
|
||||
@@ -71,11 +123,19 @@ watch(
|
||||
if (connected) {
|
||||
selectedDb.value = 0
|
||||
keyStore.currentDb = 0
|
||||
loadCustomNames()
|
||||
await fetchDbInfo()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => connStore.activeId,
|
||||
() => {
|
||||
loadCustomNames()
|
||||
}
|
||||
)
|
||||
|
||||
async function switchDb(db: number) {
|
||||
if (!connStore.activeId) return
|
||||
selectedDb.value = db
|
||||
@@ -91,14 +151,21 @@ async function switchDb(db: number) {
|
||||
:model-value="selectedDb"
|
||||
size="small"
|
||||
@change="switchDb"
|
||||
style="width: 130px"
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option
|
||||
v-for="db in dbList"
|
||||
:key="db.value"
|
||||
:label="db.label"
|
||||
:value="db.value"
|
||||
/>
|
||||
>
|
||||
<div class="db-option-content">
|
||||
<span class="db-option-label">{{ db.label }}</span>
|
||||
<el-icon class="db-edit-icon" @click="(e: MouseEvent) => handleEditName(db.value, e)">
|
||||
<EditPen />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -117,6 +184,63 @@ async function switchDb(db: number) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.db-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.db-option-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.db-edit-icon {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.db-edit-icon:hover {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper:hover) {
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper.is-focus) {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.db-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.db-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
|
||||
@@ -16,6 +16,9 @@ const themes = [
|
||||
const locales = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
]
|
||||
|
||||
const fontOptions = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// src/components/SlowLogView.vue
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
|
||||
@@ -12,6 +12,7 @@ interface SlowLogEntry {
|
||||
timestamp: number
|
||||
duration: number
|
||||
command: string
|
||||
node?: string
|
||||
}
|
||||
|
||||
const entries = ref<SlowLogEntry[]>([])
|
||||
@@ -19,6 +20,71 @@ const loading = ref(false)
|
||||
const configThreshold = ref<string | null>(null)
|
||||
const configMaxLen = ref<string | null>(null)
|
||||
|
||||
const isCluster = computed(() => connStore.activeConnection?.cluster ?? false)
|
||||
|
||||
interface ClusterNode {
|
||||
id: string
|
||||
host: string
|
||||
port: number
|
||||
flags: string[]
|
||||
slotRanges: string[]
|
||||
}
|
||||
|
||||
function parseClusterNodes(output: string): ClusterNode[] {
|
||||
const nodes: ClusterNode[] = []
|
||||
for (const line of output.trim().split('\n')) {
|
||||
if (!line.trim()) continue
|
||||
const parts = line.split(' ')
|
||||
if (parts.length < 2) continue
|
||||
const addrParts = parts[1].split(':')
|
||||
const flags = parts[2] ? parts[2].split(',') : []
|
||||
const slotRanges: string[] = []
|
||||
for (let i = 8; i < parts.length; i++) {
|
||||
if (parts[i].includes('-') || /^\d+$/.test(parts[i])) {
|
||||
slotRanges.push(parts[i])
|
||||
}
|
||||
}
|
||||
nodes.push({
|
||||
id: parts[0],
|
||||
host: addrParts[0],
|
||||
port: parseInt(addrParts[1], 10) || 6379,
|
||||
flags,
|
||||
slotRanges,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
async function fetchClusterMasterNodes(): Promise<ClusterNode[]> {
|
||||
if (!connStore.activeId) return []
|
||||
try {
|
||||
const raw = await window.electronAPI.redis.execute(connStore.activeId, 'CLUSTER', ['NODES'])
|
||||
if (typeof raw !== 'string') return []
|
||||
const allNodes = parseClusterNodes(raw)
|
||||
// Filter master nodes (those with 'master' flag and assigned slots)
|
||||
return allNodes.filter(n => n.flags.includes('master') && n.slotRanges.length > 0)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSlowLogForNode(node: ClusterNode, count: number): Promise<SlowLogEntry[]> {
|
||||
if (!connStore.activeId) return []
|
||||
try {
|
||||
const raw = await window.electronAPI.redis.execute(connStore.activeId, 'SLOWLOG', ['GET', String(count)])
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map((item: any) => ({
|
||||
id: item[0],
|
||||
timestamp: item[1],
|
||||
duration: item[2],
|
||||
command: Array.isArray(item[3]) ? item[3].join(' ') : String(item[3]),
|
||||
node: `${node.host}:${node.port}`,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
if (!connStore.activeId) return
|
||||
try {
|
||||
@@ -34,14 +100,25 @@ async function fetchConfig() {
|
||||
async function refresh() {
|
||||
if (!connStore.activeId) return
|
||||
loading.value = true
|
||||
entries.value = []
|
||||
try {
|
||||
const raw = await window.electronAPI.redis.slowLogGet(connStore.activeId, 50)
|
||||
entries.value = raw.map((item: any) => ({
|
||||
id: item[0],
|
||||
timestamp: item[1],
|
||||
duration: item[2],
|
||||
command: Array.isArray(item[3]) ? item[3].join(' ') : String(item[3]),
|
||||
}))
|
||||
if (isCluster.value) {
|
||||
// Cluster mode: fetch slowlog from each master node
|
||||
const masters = await fetchClusterMasterNodes()
|
||||
const results = await Promise.all(masters.map(m => fetchSlowLogForNode(m, 50)))
|
||||
// Flatten and sort by timestamp descending
|
||||
const all = results.flat().sort((a, b) => b.timestamp - a.timestamp)
|
||||
entries.value = all.slice(0, 200) // limit to 200 entries
|
||||
} else {
|
||||
// Standard mode: single node slowlog
|
||||
const raw = await window.electronAPI.redis.slowLogGet(connStore.activeId, 50)
|
||||
entries.value = raw.map((item: any) => ({
|
||||
id: item[0],
|
||||
timestamp: item[1],
|
||||
duration: item[2],
|
||||
command: Array.isArray(item[3]) ? item[3].join(' ') : String(item[3]),
|
||||
}))
|
||||
}
|
||||
await fetchConfig()
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -71,10 +148,16 @@ defineExpose({ refresh })
|
||||
<div class="slow-log">
|
||||
<div class="toolbar">
|
||||
<span class="title">{{ t('slowlog.title') }}</span>
|
||||
<span v-if="isCluster" class="cluster-badge">CLUSTER</span>
|
||||
<el-button size="small" @click="refresh" :loading="loading">{{ t('common.refresh') }}</el-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-table :data="entries" size="small" max-height="400" :empty-text="t('common.noData')">
|
||||
<el-table-column v-if="isCluster" :label="'Node'" width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="node-text">{{ row.node }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('slowlog.time')" width="180">
|
||||
<template #default="{ row }">{{ formatTime(row.timestamp) }}</template>
|
||||
</el-table-column>
|
||||
@@ -117,6 +200,67 @@ defineExpose({ refresh })
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.cluster-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
margin-left: 8px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cmd-text {
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-text {
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.config-footer {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.slow-log {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
|
||||
@@ -22,7 +22,22 @@ interface KeyStatEntry {
|
||||
avgTtl: number | null
|
||||
}
|
||||
|
||||
interface ClusterInfo {
|
||||
cluster_state: string
|
||||
cluster_slots_assigned: string
|
||||
cluster_slots_ok: string
|
||||
cluster_slots_pfail: string
|
||||
cluster_slots_fail: string
|
||||
cluster_known_nodes: string
|
||||
cluster_size: string
|
||||
cluster_current_epoch: string
|
||||
cluster_my_epoch: string
|
||||
cluster_stats_messages_sent: string
|
||||
cluster_stats_messages_received: string
|
||||
}
|
||||
|
||||
const sections = ref<InfoSection[]>([])
|
||||
const clusterInfo = ref<ClusterInfo | null>(null)
|
||||
|
||||
// Auto-refresh toggle state (persisted to localStorage, default ON)
|
||||
const autoRefreshEnabled = ref(localStorage.getItem('statusAutoRefresh') !== 'false')
|
||||
@@ -129,11 +144,51 @@ function parseInfo(raw: string): InfoSection[] {
|
||||
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function fetchClusterInfo() {
|
||||
if (!connStore.activeId) return
|
||||
try {
|
||||
const raw = await window.electronAPI.redis.execute(connStore.activeId, 'CLUSTER', ['INFO'])
|
||||
if (typeof raw !== 'string') {
|
||||
clusterInfo.value = null
|
||||
return
|
||||
}
|
||||
const parsed: Record<string, string> = {}
|
||||
for (const line of raw.split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) continue
|
||||
const colonIdx = trimmed.indexOf(':')
|
||||
if (colonIdx > 0) {
|
||||
parsed[trimmed.slice(0, colonIdx)] = trimmed.slice(colonIdx + 1)
|
||||
}
|
||||
}
|
||||
clusterInfo.value = {
|
||||
cluster_state: parsed.cluster_state || '',
|
||||
cluster_slots_assigned: parsed.cluster_slots_assigned || '',
|
||||
cluster_slots_ok: parsed.cluster_slots_ok || '',
|
||||
cluster_slots_pfail: parsed.cluster_slots_pfail || '',
|
||||
cluster_slots_fail: parsed.cluster_slots_fail || '',
|
||||
cluster_known_nodes: parsed.cluster_known_nodes || '',
|
||||
cluster_size: parsed.cluster_size || '',
|
||||
cluster_current_epoch: parsed.cluster_current_epoch || '',
|
||||
cluster_my_epoch: parsed.cluster_my_epoch || '',
|
||||
cluster_stats_messages_sent: parsed.cluster_stats_messages_sent || '',
|
||||
cluster_stats_messages_received: parsed.cluster_stats_messages_received || '',
|
||||
}
|
||||
} catch {
|
||||
clusterInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!connStore.activeId) return
|
||||
info.value = await connStore.getInfo()
|
||||
sections.value = parseInfo(info.value)
|
||||
dbSize.value = await window.electronAPI.redis.dbSize(connStore.activeId)
|
||||
if (connStore.activeConnection?.cluster) {
|
||||
await fetchClusterInfo()
|
||||
} else {
|
||||
clusterInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
@@ -243,6 +298,57 @@ defineExpose({ refresh })
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- Cluster Info card -->
|
||||
<div v-if="clusterInfo" class="cluster-info-section">
|
||||
<h3 class="section-title">Cluster Info</h3>
|
||||
<div class="cluster-info-grid">
|
||||
<div class="cluster-info-item" :class="{ 'state-ok': clusterInfo.cluster_state === 'ok', 'state-fail': clusterInfo.cluster_state !== 'ok' }">
|
||||
<span class="cluster-info-key">cluster_state</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_state }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_slots_assigned</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_slots_assigned }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_slots_ok</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_slots_ok }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_slots_pfail</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_slots_pfail }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_slots_fail</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_slots_fail }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_known_nodes</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_known_nodes }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_size</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_size }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_current_epoch</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_current_epoch }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_my_epoch</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_my_epoch }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_stats_messages_sent</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_stats_messages_sent }}</span>
|
||||
</div>
|
||||
<div class="cluster-info-item">
|
||||
<span class="cluster-info-key">cluster_stats_messages_received</span>
|
||||
<span class="cluster-info-value">{{ clusterInfo.cluster_stats_messages_received }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-sections">
|
||||
<template v-if="filteredSections.length > 0">
|
||||
<div v-for="section in filteredSections" :key="section.title" class="info-section">
|
||||
@@ -391,6 +497,67 @@ defineExpose({ refresh })
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
}
|
||||
|
||||
.cluster-info-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cluster-info-section .section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-light);
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cluster-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cluster-info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.cluster-info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cluster-info-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.cluster-info-key {
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cluster-info-value {
|
||||
color: var(--text-primary);
|
||||
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cluster-info-item.state-ok .cluster-info-value {
|
||||
color: var(--color-success, #67c23a);
|
||||
}
|
||||
|
||||
.cluster-info-item.state-fail .cluster-info-value {
|
||||
color: var(--color-danger, #f56c6c);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import en from './locales/en'
|
||||
import zhCN from './locales/zh-CN'
|
||||
import ja from './locales/ja'
|
||||
import ko from './locales/ko'
|
||||
import de from './locales/de'
|
||||
|
||||
type Messages = typeof en
|
||||
|
||||
const messages: Record<string, Messages> = {
|
||||
en,
|
||||
'zh-CN': zhCN,
|
||||
ja,
|
||||
ko,
|
||||
de,
|
||||
}
|
||||
|
||||
const currentLocale = ref<string>(
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
// src/i18n/locales/de.ts
|
||||
export default {
|
||||
common: {
|
||||
ok: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
save: 'Speichern',
|
||||
delete: 'Löschen',
|
||||
edit: 'Bearbeiten',
|
||||
add: 'Hinzufügen',
|
||||
refresh: 'Aktualisieren',
|
||||
search: 'Suchen',
|
||||
confirm: 'Bestätigen',
|
||||
success: 'Erfolg',
|
||||
error: 'Fehler',
|
||||
loading: 'Laden...',
|
||||
noData: 'Keine Daten',
|
||||
},
|
||||
app: {
|
||||
title: 'JRedisDesktop',
|
||||
subtitle: 'Ein moderner Redis-Desktop-Manager',
|
||||
placeholder: 'Wählen oder erstellen Sie eine Verbindung',
|
||||
},
|
||||
tab: {
|
||||
close: 'Schließen',
|
||||
closeOthers: 'Andere schließen',
|
||||
closeRight: 'Rechts schließen',
|
||||
closeLeft: 'Links schließen',
|
||||
},
|
||||
connection: {
|
||||
title: 'Verbindungen',
|
||||
new: 'Neue Verbindung',
|
||||
edit: 'Verbindung bearbeiten',
|
||||
name: 'Name',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
password: 'Passwort',
|
||||
username: 'Benutzername',
|
||||
tls: 'TLS aktivieren',
|
||||
tlsCaPath: 'CA-Zertifikat',
|
||||
tlsCertPath: 'Client-Zertifikat',
|
||||
tlsKeyPath: 'Client-privater Schlüssel',
|
||||
tlsRejectUnauthorized: 'Serverzertifikat überprüfen',
|
||||
sentinel: 'Sentinel-Modus',
|
||||
sentinelHost: 'Sentinel-Host',
|
||||
sentinelPort: 'Sentinel-Port',
|
||||
sentinelMasterName: 'Master-Name',
|
||||
sentinelNodePassword: 'Sentinel-Passwort',
|
||||
cluster: 'Cluster-Modus',
|
||||
ssh: 'SSH-Tunnel',
|
||||
sshHost: 'SSH-Host',
|
||||
sshPort: 'SSH-Port',
|
||||
sshUsername: 'SSH-Benutzername',
|
||||
sshPassword: 'SSH-Passwort',
|
||||
sshPrivateKey: 'Privater Schlüsselpfad',
|
||||
sshPassphrase: 'Passphrase',
|
||||
selectFile: 'Datei auswählen',
|
||||
connect: 'Verbinden',
|
||||
disconnect: 'Trennen',
|
||||
deleteConfirm: 'Diese Verbindung löschen?',
|
||||
testSuccess: 'Erfolgreich verbunden',
|
||||
testFailed: 'Verbindung fehlgeschlagen',
|
||||
connected: 'Verbunden',
|
||||
disconnected: 'Getrennt',
|
||||
search: 'Verbindungen durchsuchen...',
|
||||
noMatch: 'Keine passenden Verbindungen',
|
||||
noConnections: 'Noch keine Verbindungen',
|
||||
import: 'Importieren',
|
||||
export: 'Exportieren',
|
||||
testConnection: 'Verbindung testen',
|
||||
create: 'Erstellen',
|
||||
save: 'Speichern',
|
||||
duplicate: 'Duplizieren',
|
||||
setColor: 'Farbe festlegen',
|
||||
duplicated: 'Verbindung dupliziert',
|
||||
},
|
||||
key: {
|
||||
title: 'Schlüssel',
|
||||
filter: 'Schlüssel filtern...',
|
||||
loadMore: 'Mehr laden...',
|
||||
count: '{n} Schlüssel',
|
||||
deleteConfirm: 'Schlüssel "{key}" löschen?',
|
||||
deleted: 'Schlüssel gelöscht',
|
||||
rename: 'Umbenennen',
|
||||
renameTitle: 'Schlüssel umbenennen',
|
||||
newName: 'Neuer Name',
|
||||
renamed: 'Schlüssel umbenannt',
|
||||
copyName: 'Schlüsselname kopieren',
|
||||
copied: 'Kopiert',
|
||||
ttl: 'TTL',
|
||||
ttlTitle: 'TTL festlegen',
|
||||
ttlHint: '-1 = kein Ablauf, 0 = sofort ablaufen',
|
||||
ttlUpdated: 'TTL aktualisiert',
|
||||
exists: 'Schlüssel existiert bereits',
|
||||
batchDelete: 'Ausgewählte löschen',
|
||||
batchConfirm: '{n} Schlüssel löschen?',
|
||||
batchDeleted: '{n} Schlüssel gelöscht',
|
||||
batchPreview: 'Löschvorschau',
|
||||
batchPreviewHint: '{n} Schlüssel werden gelöscht',
|
||||
andMore: '...und {n} weitere',
|
||||
selected: '{n} ausgewählt',
|
||||
selectKey: 'Wählen Sie einen Schlüssel für Details',
|
||||
unsupported: 'Typ "{type}"-Anzeige noch nicht implementiert',
|
||||
patternPlaceholder: 'Schlüsselmuster (z.B. user:*)',
|
||||
searchHistory: 'Suchverlauf',
|
||||
clearHistory: 'Verlauf löschen',
|
||||
noHistory: 'Kein Verlauf',
|
||||
cancelScan: 'Scan abbrechen',
|
||||
scanCancelled: 'Scan abgebrochen',
|
||||
switchTree: 'Zur Baumansicht wechseln',
|
||||
switchList: 'Zur Listenansicht wechseln',
|
||||
exportKeys: 'Schlüssel exportieren',
|
||||
importKeys: 'Schlüssel importieren',
|
||||
exportDone: '{n} Schlüssel exportiert',
|
||||
importDone: '{n} Schlüssel importiert',
|
||||
importFailed: 'Import fehlgeschlagen: {error}',
|
||||
binaryKey: 'Binärschlüssel',
|
||||
copyHex: 'Hex kopieren',
|
||||
copyKeyName: 'Schlüsselname kopieren',
|
||||
copyValue: 'Wert kopieren',
|
||||
memoryUsage: 'Speichernutzung',
|
||||
memoryUsageResult: 'Speichernutzung: {size} Bytes',
|
||||
selectAll: 'Alle auswählen',
|
||||
},
|
||||
types: {
|
||||
string: 'STRING',
|
||||
hash: 'HASH',
|
||||
list: 'LIST',
|
||||
set: 'SET',
|
||||
zset: 'ZSET',
|
||||
stream: 'STREAM',
|
||||
},
|
||||
editor: {
|
||||
save: 'Speichern',
|
||||
cancel: 'Abbrechen',
|
||||
edit: 'Bearbeiten',
|
||||
saved: 'Gespeichert',
|
||||
push: 'Hinzufügen',
|
||||
addField: 'Feld hinzufügen',
|
||||
addMember: 'Mitglied hinzufügen',
|
||||
addEntry: 'Eintrag hinzufügen',
|
||||
filter: 'Filtern...',
|
||||
refresh: 'Aktualisieren',
|
||||
elements: '{n} Elemente',
|
||||
members: '{n} Mitglieder',
|
||||
empty: 'Leer',
|
||||
field: 'Feld',
|
||||
value: 'Wert',
|
||||
score: 'Punktzahl',
|
||||
member: 'Mitglied',
|
||||
remove: 'Entfernen',
|
||||
removed: 'Entfernt',
|
||||
added: 'Hinzugefügt',
|
||||
trim: 'Kürzen',
|
||||
trimConfirm: 'Stream kürzen?',
|
||||
entryId: 'ID (* für automatisch)',
|
||||
fields: 'Felder',
|
||||
maxLength: 'Maximale Länge',
|
||||
format: 'Format',
|
||||
formatAuto: 'Automatisch',
|
||||
formatText: 'Text',
|
||||
formatHex: 'Hex',
|
||||
formatJson: 'JSON',
|
||||
formatBinary: 'Binär',
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
formatCustom: 'Benutzerdefiniert',
|
||||
customCommand: 'Benutzerdefinierter Befehl',
|
||||
customCommandHint: 'Vorlagen: {KEY} {VALUE} {HEX_FILE}',
|
||||
consumerGroups: 'Verbrauchergruppen',
|
||||
group: 'Gruppe',
|
||||
consumers: 'Verbraucher',
|
||||
pending: 'Ausstehend',
|
||||
createGroup: 'Gruppe erstellen',
|
||||
destroyGroup: 'Gruppe löschen',
|
||||
groupName: 'Gruppenname',
|
||||
startId: 'Start-ID',
|
||||
mkstream: 'Stream automatisch erstellen',
|
||||
ack: 'ACK',
|
||||
ackConfirm: 'Ausgewählte Einträge bestätigen?',
|
||||
noGroups: 'Keine Verbrauchergruppen',
|
||||
valueTooLarge: 'Wert zu groß für Anzeige (>20MB)',
|
||||
download: 'Herunterladen',
|
||||
setFieldTtl: 'Feld-TTL festlegen',
|
||||
fieldTtl: 'Feld-TTL',
|
||||
sortAsc: 'Aufsteigend',
|
||||
sortDesc: 'Absteigend',
|
||||
filteredCount: '{n} gefiltert',
|
||||
fieldCount: '{n} Felder',
|
||||
},
|
||||
cli: {
|
||||
title: 'CLI',
|
||||
welcome: 'Redis CLI - Befehle unten eingeben',
|
||||
placeholder: 'Redis-Befehl eingeben...',
|
||||
stopSubscribe: 'Abonnement beenden',
|
||||
stopMonitor: 'Monitor beenden',
|
||||
subscribed: 'Abonniert, warte auf Nachrichten...',
|
||||
monitoring: 'Überwachung läuft, warte auf Befehle...',
|
||||
subscribedTo: 'Abonniert: {channels}',
|
||||
txStarted: 'Transaktion gestartet, Befehle werden in die Warteschlange gestellt...',
|
||||
txQueued: 'EINGEREIHT',
|
||||
txExecuted: 'Transaktion ausgeführt',
|
||||
txDiscarded: 'Transaktion verworfen',
|
||||
txQueue: 'Warteschlange ({n})',
|
||||
txPrompt: 'tx>',
|
||||
importCommands: 'Befehle importieren',
|
||||
importCmdDone: '{n} Befehle ausgeführt',
|
||||
hotkeys: 'Tastenkombinationen',
|
||||
hotkeysTitle: 'Tastaturkürzel',
|
||||
},
|
||||
slowlog: {
|
||||
title: 'Langsames Log',
|
||||
time: 'Zeit',
|
||||
duration: 'Dauer',
|
||||
command: 'Befehl',
|
||||
threshold: 'Schwellenwert',
|
||||
maxLen: 'Maximale Länge',
|
||||
},
|
||||
memory: {
|
||||
title: 'Speicheranalyse',
|
||||
scan: 'Scan starten',
|
||||
pause: 'Pause',
|
||||
resume: 'Fortsetzen',
|
||||
stop: 'Stopp',
|
||||
scanning: 'Scanne...',
|
||||
paused: 'Pausiert',
|
||||
noData: 'Keine Daten',
|
||||
key: 'Schlüssel',
|
||||
size: 'Größe',
|
||||
type: 'Typ',
|
||||
totalSize: 'Gesamtgröße',
|
||||
minSize: 'Mindestgröße (KB)',
|
||||
sortAsc: 'Aufsteigend',
|
||||
sortDesc: 'Absteigend',
|
||||
scanned: '{n} Schlüssel gescannt',
|
||||
},
|
||||
commandLog: {
|
||||
title: 'Befehlslog',
|
||||
time: 'Zeit',
|
||||
command: 'Befehl',
|
||||
duration: 'Dauer',
|
||||
connection: 'Verbindung',
|
||||
onlyWrite: 'Nur Schreiben',
|
||||
clear: 'Löschen',
|
||||
empty: 'Keine Befehle aufgezeichnet',
|
||||
},
|
||||
updater: {
|
||||
checking: 'Suche nach Updates...',
|
||||
available: 'Neue Version {version} verfügbar',
|
||||
notAvailable: 'Sie sind auf dem neuesten Stand',
|
||||
downloading: 'Lade herunter... {percent}%',
|
||||
downloaded: 'Update heruntergeladen. Jetzt neu starten?',
|
||||
install: 'Jetzt neu starten',
|
||||
later: 'Später',
|
||||
error: 'Update fehlgeschlagen: {message}',
|
||||
},
|
||||
status: {
|
||||
title: 'Status',
|
||||
database: 'Datenbank',
|
||||
keys: 'Schlüssel',
|
||||
refresh: 'Aktualisieren',
|
||||
flushDb: 'DB leeren',
|
||||
flushDbConfirm: 'Dies löscht ALLE Schlüssel in der aktuellen Datenbank! Geben Sie FLUSHDB zur Bestätigung ein:',
|
||||
flushDbDone: 'Datenbank geleert',
|
||||
connecting: 'Verbinde...',
|
||||
error: 'Fehler: {error}',
|
||||
noConnection: 'Keine Verbindung',
|
||||
autoRefresh: 'Automatisch aktualisieren',
|
||||
searchInfo: 'INFO durchsuchen...',
|
||||
noResults: 'Keine Ergebnisse',
|
||||
keyStats: 'Schlüsselstatistik',
|
||||
dbColumn: 'Datenbank',
|
||||
keysColumn: 'Schlüssel',
|
||||
expiresColumn: 'Abläufe',
|
||||
avgTtlColumn: 'Durchschn. TTL',
|
||||
sectionServer: 'Server',
|
||||
sectionClients: 'Clients',
|
||||
sectionMemory: 'Speicher',
|
||||
sectionPersistence: 'Persistenz',
|
||||
sectionStats: 'Statistiken',
|
||||
sectionReplication: 'Replikation',
|
||||
sectionCpu: 'CPU',
|
||||
sectionModules: 'Module',
|
||||
sectionErrorstats: 'Fehlerstatistiken',
|
||||
sectionCluster: 'Cluster',
|
||||
sectionKeyspace: 'Keyspace',
|
||||
},
|
||||
db: {
|
||||
label: 'DB',
|
||||
keyCount: '{n} Schlüssel',
|
||||
noKeys: 'leer',
|
||||
},
|
||||
settings: {
|
||||
title: 'Einstellungen',
|
||||
theme: 'Design',
|
||||
themeDark: 'Dunkel',
|
||||
themeLight: 'Hell',
|
||||
themeSystem: 'System',
|
||||
language: 'Sprache',
|
||||
scanCount: 'Schlüssel pro SCAN',
|
||||
fontSize: 'Schriftgröße',
|
||||
zoom: 'Seitenzoom',
|
||||
fontFamily: 'Monospace-Schriftart',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// src/i18n/locales/ja.ts
|
||||
export default {
|
||||
common: {
|
||||
ok: 'OK',
|
||||
cancel: 'キャンセル',
|
||||
save: '保存',
|
||||
delete: '削除',
|
||||
edit: '編集',
|
||||
add: '追加',
|
||||
refresh: '更新',
|
||||
search: '検索',
|
||||
confirm: '確認',
|
||||
success: '成功',
|
||||
error: 'エラー',
|
||||
loading: '読み込み中...',
|
||||
noData: 'データなし',
|
||||
},
|
||||
app: {
|
||||
title: 'JRedisDesktop',
|
||||
subtitle: 'モダンなRedisデスクトップマネージャー',
|
||||
placeholder: '接続を選択または作成して開始',
|
||||
},
|
||||
tab: {
|
||||
close: '閉じる',
|
||||
closeOthers: '他を閉じる',
|
||||
closeRight: '右を閉じる',
|
||||
closeLeft: '左を閉じる',
|
||||
},
|
||||
connection: {
|
||||
title: '接続',
|
||||
new: '新規接続',
|
||||
edit: '接続を編集',
|
||||
name: '名前',
|
||||
host: 'ホスト',
|
||||
port: 'ポート',
|
||||
password: 'パスワード',
|
||||
username: 'ユーザー名',
|
||||
tls: 'TLSを有効化',
|
||||
tlsCaPath: 'CA証明書',
|
||||
tlsCertPath: 'クライアント証明書',
|
||||
tlsKeyPath: 'クライアント秘密鍵',
|
||||
tlsRejectUnauthorized: 'サーバー証明書を検証',
|
||||
sentinel: 'Sentinelモード',
|
||||
sentinelHost: 'Sentinelホスト',
|
||||
sentinelPort: 'Sentinelポート',
|
||||
sentinelMasterName: 'マスター名',
|
||||
sentinelNodePassword: 'Sentinelパスワード',
|
||||
cluster: 'クラスターモード',
|
||||
ssh: 'SSHトンネル',
|
||||
sshHost: 'SSHホスト',
|
||||
sshPort: 'SSHポート',
|
||||
sshUsername: 'SSHユーザー名',
|
||||
sshPassword: 'SSHパスワード',
|
||||
sshPrivateKey: '秘密鍵パス',
|
||||
sshPassphrase: 'パスフレーズ',
|
||||
selectFile: 'ファイルを選択',
|
||||
connect: '接続',
|
||||
disconnect: '切断',
|
||||
deleteConfirm: 'この接続を削除しますか?',
|
||||
testSuccess: '接続に成功しました',
|
||||
testFailed: '接続に失敗しました',
|
||||
connected: '接続済み',
|
||||
disconnected: '未接続',
|
||||
search: '接続を検索...',
|
||||
noMatch: '一致する接続がありません',
|
||||
noConnections: '接続がまだありません',
|
||||
import: 'インポート',
|
||||
export: 'エクスポート',
|
||||
testConnection: '接続テスト',
|
||||
create: '作成',
|
||||
save: '保存',
|
||||
duplicate: '複製',
|
||||
setColor: '色を設定',
|
||||
duplicated: '接続が複製されました',
|
||||
},
|
||||
key: {
|
||||
title: 'キー',
|
||||
filter: 'キーをフィルター...',
|
||||
loadMore: 'さらに読み込む...',
|
||||
count: '{n} 個のキー',
|
||||
deleteConfirm: 'キー "{key}" を削除しますか?',
|
||||
deleted: 'キーを削除しました',
|
||||
rename: '名前変更',
|
||||
renameTitle: 'キーの名前変更',
|
||||
newName: '新しい名前',
|
||||
renamed: 'キーの名前を変更しました',
|
||||
copyName: 'キー名をコピー',
|
||||
copied: 'コピーしました',
|
||||
ttl: 'TTL',
|
||||
ttlTitle: 'TTLを設定',
|
||||
ttlHint: '-1 = 無期限, 0 = 即時期限切れ',
|
||||
ttlUpdated: 'TTLを更新しました',
|
||||
exists: 'キーは既に存在します',
|
||||
batchDelete: '選択を削除',
|
||||
batchConfirm: '{n} 個のキーを削除しますか?',
|
||||
batchDeleted: '{n} 個のキーを削除しました',
|
||||
batchPreview: '削除プレビュー',
|
||||
batchPreviewHint: '{n} 個のキーを削除しようとしています',
|
||||
andMore: '...他 {n} 個',
|
||||
selected: '{n} 個選択済み',
|
||||
selectKey: 'キーを選択して詳細を表示',
|
||||
unsupported: 'タイプ "{type}" のビューアーは未実装です',
|
||||
patternPlaceholder: 'キーパターン (例: user:*)',
|
||||
searchHistory: '検索履歴',
|
||||
clearHistory: '履歴をクリア',
|
||||
noHistory: '履歴なし',
|
||||
cancelScan: 'スキャンをキャンセル',
|
||||
scanCancelled: 'スキャンがキャンセルされました',
|
||||
switchTree: 'ツリー表示に切り替え',
|
||||
switchList: 'リスト表示に切り替え',
|
||||
exportKeys: 'キーをエクスポート',
|
||||
importKeys: 'キーをインポート',
|
||||
exportDone: '{n} 個のキーをエクスポートしました',
|
||||
importDone: '{n} 個のキーをインポートしました',
|
||||
importFailed: 'インポートに失敗しました: {error}',
|
||||
binaryKey: 'バイナリキー',
|
||||
copyHex: 'Hexをコピー',
|
||||
copyKeyName: 'キー名をコピー',
|
||||
copyValue: '値をコピー',
|
||||
memoryUsage: 'メモリ使用量',
|
||||
memoryUsageResult: 'メモリ使用量: {size} バイト',
|
||||
selectAll: 'すべて選択',
|
||||
},
|
||||
types: {
|
||||
string: 'STRING',
|
||||
hash: 'HASH',
|
||||
list: 'LIST',
|
||||
set: 'SET',
|
||||
zset: 'ZSET',
|
||||
stream: 'STREAM',
|
||||
},
|
||||
editor: {
|
||||
save: '保存',
|
||||
cancel: 'キャンセル',
|
||||
edit: '編集',
|
||||
saved: '保存しました',
|
||||
push: 'プッシュ',
|
||||
addField: 'フィールドを追加',
|
||||
addMember: 'メンバーを追加',
|
||||
addEntry: 'エントリを追加',
|
||||
filter: 'フィルター...',
|
||||
refresh: '更新',
|
||||
elements: '{n} 個の要素',
|
||||
members: '{n} 個のメンバー',
|
||||
empty: '空',
|
||||
field: 'フィールド',
|
||||
value: '値',
|
||||
score: 'スコア',
|
||||
member: 'メンバー',
|
||||
remove: '削除',
|
||||
removed: '削除しました',
|
||||
added: '追加しました',
|
||||
trim: 'トリム',
|
||||
trimConfirm: 'ストリームをトリムしますか?',
|
||||
entryId: 'ID (* で自動生成)',
|
||||
fields: 'フィールド',
|
||||
maxLength: '最大長',
|
||||
format: 'フォーマット',
|
||||
formatAuto: '自動検出',
|
||||
formatText: 'テキスト',
|
||||
formatHex: 'Hex',
|
||||
formatJson: 'JSON',
|
||||
formatBinary: 'バイナリ',
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
formatCustom: 'カスタム',
|
||||
customCommand: 'カスタムコマンド',
|
||||
customCommandHint: 'テンプレート: {KEY} {VALUE} {HEX_FILE}',
|
||||
consumerGroups: 'コンシューマーグループ',
|
||||
group: 'グループ',
|
||||
consumers: 'コンシューマー',
|
||||
pending: '保留中',
|
||||
createGroup: 'グループを作成',
|
||||
destroyGroup: 'グループを削除',
|
||||
groupName: 'グループ名',
|
||||
startId: '開始ID',
|
||||
mkstream: 'ストリームを自動作成',
|
||||
ack: 'ACK',
|
||||
ackConfirm: '選択したエントリをACKしますか?',
|
||||
noGroups: 'コンシューマーグループがありません',
|
||||
valueTooLarge: '値が大きすぎて表示できません (>20MB)',
|
||||
download: 'ダウンロード',
|
||||
setFieldTtl: 'フィールドTTLを設定',
|
||||
fieldTtl: 'フィールドTTL',
|
||||
sortAsc: '昇順',
|
||||
sortDesc: '降順',
|
||||
filteredCount: '{n} 個フィルター済み',
|
||||
fieldCount: '{n} 個のフィールド',
|
||||
},
|
||||
cli: {
|
||||
title: 'CLI',
|
||||
welcome: 'Redis CLI - 以下にコマンドを入力',
|
||||
placeholder: 'Redisコマンドを入力...',
|
||||
stopSubscribe: 'サブスクライブ停止',
|
||||
stopMonitor: 'モニター停止',
|
||||
subscribed: 'サブスクライブ中、メッセージを待機...',
|
||||
monitoring: 'モニタリング中、コマンドを待機...',
|
||||
subscribedTo: 'サブスクライブ中: {channels}',
|
||||
txStarted: 'トランザクション開始、コマンドはキューに入れられています...',
|
||||
txQueued: 'キューイング済み',
|
||||
txExecuted: 'トランザクション実行済み',
|
||||
txDiscarded: 'トランザクション破棄済み',
|
||||
txQueue: 'キュー ({n})',
|
||||
txPrompt: 'tx>',
|
||||
importCommands: 'コマンドをインポート',
|
||||
importCmdDone: '{n} 個のコマンドを実行しました',
|
||||
hotkeys: 'ホットキー',
|
||||
hotkeysTitle: 'キーボードショートカット',
|
||||
},
|
||||
slowlog: {
|
||||
title: 'スローログ',
|
||||
time: '時間',
|
||||
duration: '所要時間',
|
||||
command: 'コマンド',
|
||||
threshold: 'しきい値',
|
||||
maxLen: '最大長',
|
||||
},
|
||||
memory: {
|
||||
title: 'メモリ分析',
|
||||
scan: 'スキャン開始',
|
||||
pause: '一時停止',
|
||||
resume: '再開',
|
||||
stop: '停止',
|
||||
scanning: 'スキャン中...',
|
||||
paused: '一時停止中',
|
||||
noData: 'データなし',
|
||||
key: 'キー',
|
||||
size: 'サイズ',
|
||||
type: 'タイプ',
|
||||
totalSize: '合計サイズ',
|
||||
minSize: '最小サイズ (KB)',
|
||||
sortAsc: '昇順',
|
||||
sortDesc: '降順',
|
||||
scanned: '{n} 個のキーをスキャンしました',
|
||||
},
|
||||
commandLog: {
|
||||
title: 'コマンドログ',
|
||||
time: '時間',
|
||||
command: 'コマンド',
|
||||
duration: '所要時間',
|
||||
connection: '接続',
|
||||
onlyWrite: '書き込みのみ',
|
||||
clear: 'クリア',
|
||||
empty: '記録されたコマンドはありません',
|
||||
},
|
||||
updater: {
|
||||
checking: 'アップデートを確認中...',
|
||||
available: '新しいバージョン {version} が利用可能です',
|
||||
notAvailable: '最新バージョンです',
|
||||
downloading: 'ダウンロード中... {percent}%',
|
||||
downloaded: 'アップデートをダウンロードしました。今すぐ再起動しますか?',
|
||||
install: '今すぐ再起動',
|
||||
later: '後で',
|
||||
error: 'アップデートに失敗しました: {message}',
|
||||
},
|
||||
status: {
|
||||
title: 'ステータス',
|
||||
database: 'データベース',
|
||||
keys: 'キー数',
|
||||
refresh: '更新',
|
||||
flushDb: 'DBをフラッシュ',
|
||||
flushDbConfirm: '現在のデータベースのすべてのキーが削除されます!確認のため FLUSHDB と入力してください:',
|
||||
flushDbDone: 'データベースをフラッシュしました',
|
||||
connecting: '接続中...',
|
||||
error: 'エラー: {error}',
|
||||
noConnection: '接続なし',
|
||||
autoRefresh: '自動更新',
|
||||
searchInfo: 'INFOを検索...',
|
||||
noResults: '結果なし',
|
||||
keyStats: 'キー統計',
|
||||
dbColumn: 'データベース',
|
||||
keysColumn: 'キー数',
|
||||
expiresColumn: '期限切れ',
|
||||
avgTtlColumn: '平均TTL',
|
||||
sectionServer: 'サーバー',
|
||||
sectionClients: 'クライアント',
|
||||
sectionMemory: 'メモリ',
|
||||
sectionPersistence: '永続化',
|
||||
sectionStats: '統計',
|
||||
sectionReplication: 'レプリケーション',
|
||||
sectionCpu: 'CPU',
|
||||
sectionModules: 'モジュール',
|
||||
sectionErrorstats: 'エラー統計',
|
||||
sectionCluster: 'クラスター',
|
||||
sectionKeyspace: 'キースペース',
|
||||
},
|
||||
db: {
|
||||
label: 'DB',
|
||||
keyCount: '{n} 個のキー',
|
||||
noKeys: '空',
|
||||
},
|
||||
settings: {
|
||||
title: '設定',
|
||||
theme: 'テーマ',
|
||||
themeDark: 'ダーク',
|
||||
themeLight: 'ライト',
|
||||
themeSystem: 'システム',
|
||||
language: '言語',
|
||||
scanCount: 'SCANあたりのキー数',
|
||||
fontSize: 'フォントサイズ',
|
||||
zoom: 'ページズーム',
|
||||
fontFamily: '等幅フォント',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// src/i18n/locales/ko.ts
|
||||
export default {
|
||||
common: {
|
||||
ok: '확인',
|
||||
cancel: '취소',
|
||||
save: '저장',
|
||||
delete: '삭제',
|
||||
edit: '편집',
|
||||
add: '추가',
|
||||
refresh: '새로고침',
|
||||
search: '검색',
|
||||
confirm: '확인',
|
||||
success: '성공',
|
||||
error: '오류',
|
||||
loading: '로딩 중...',
|
||||
noData: '데이터 없음',
|
||||
},
|
||||
app: {
|
||||
title: 'JRedisDesktop',
|
||||
subtitle: '모던 Redis 데스크톱 관리자',
|
||||
placeholder: '연결을 선택하거나 생성하여 시작하세요',
|
||||
},
|
||||
tab: {
|
||||
close: '닫기',
|
||||
closeOthers: '다른 탭 닫기',
|
||||
closeRight: '오른쪽 닫기',
|
||||
closeLeft: '왼쪽 닫기',
|
||||
},
|
||||
connection: {
|
||||
title: '연결',
|
||||
new: '새 연결',
|
||||
edit: '연결 편집',
|
||||
name: '이름',
|
||||
host: '호스트',
|
||||
port: '포트',
|
||||
password: '비밀번호',
|
||||
username: '사용자 이름',
|
||||
tls: 'TLS 활성화',
|
||||
tlsCaPath: 'CA 인증서',
|
||||
tlsCertPath: '클라이언트 인증서',
|
||||
tlsKeyPath: '클라이언트 개인 키',
|
||||
tlsRejectUnauthorized: '서버 인증서 확인',
|
||||
sentinel: 'Sentinel 모드',
|
||||
sentinelHost: 'Sentinel 호스트',
|
||||
sentinelPort: 'Sentinel 포트',
|
||||
sentinelMasterName: '마스터 이름',
|
||||
sentinelNodePassword: 'Sentinel 비밀번호',
|
||||
cluster: '클러스터 모드',
|
||||
ssh: 'SSH 터널',
|
||||
sshHost: 'SSH 호스트',
|
||||
sshPort: 'SSH 포트',
|
||||
sshUsername: 'SSH 사용자 이름',
|
||||
sshPassword: 'SSH 비밀번호',
|
||||
sshPrivateKey: '개인 키 경로',
|
||||
sshPassphrase: '암호',
|
||||
selectFile: '파일 선택',
|
||||
connect: '연결',
|
||||
disconnect: '연결 해제',
|
||||
deleteConfirm: '이 연결을 삭제하시겠습니까?',
|
||||
testSuccess: '연결 성공',
|
||||
testFailed: '연결 실패',
|
||||
connected: '연결됨',
|
||||
disconnected: '연결 해제됨',
|
||||
search: '연결 검색...',
|
||||
noMatch: '일치하는 연결 없음',
|
||||
noConnections: '연결이 아직 없습니다',
|
||||
import: '가져오기',
|
||||
export: '내보내기',
|
||||
testConnection: '연결 테스트',
|
||||
create: '생성',
|
||||
save: '저장',
|
||||
duplicate: '복제',
|
||||
setColor: '색상 설정',
|
||||
duplicated: '연결이 복제되었습니다',
|
||||
},
|
||||
key: {
|
||||
title: '키',
|
||||
filter: '키 필터...',
|
||||
loadMore: '더 불러오기...',
|
||||
count: '{n}개 키',
|
||||
deleteConfirm: '키 "{key}"를 삭제하시겠습니까?',
|
||||
deleted: '키가 삭제되었습니다',
|
||||
rename: '이름 변경',
|
||||
renameTitle: '키 이름 변경',
|
||||
newName: '새 이름',
|
||||
renamed: '키 이름이 변경되었습니다',
|
||||
copyName: '키 이름 복사',
|
||||
copied: '복사됨',
|
||||
ttl: 'TTL',
|
||||
ttlTitle: 'TTL 설정',
|
||||
ttlHint: '-1 = 만료 없음, 0 = 즉시 만료',
|
||||
ttlUpdated: 'TTL이 업데이트되었습니다',
|
||||
exists: '키가 이미 존재합니다',
|
||||
batchDelete: '선택 삭제',
|
||||
batchConfirm: '{n}개 키를 삭제하시겠습니까?',
|
||||
batchDeleted: '{n}개 키가 삭제되었습니다',
|
||||
batchPreview: '삭제 미리보기',
|
||||
batchPreviewHint: '{n}개 키를 삭제하려고 합니다',
|
||||
andMore: '...그 외 {n}개',
|
||||
selected: '{n}개 선택됨',
|
||||
selectKey: '키를 선택하여 세부 정보 보기',
|
||||
unsupported: '"{type}" 유형 뷰어가 아직 구현되지 않았습니다',
|
||||
patternPlaceholder: '키 패턴 (예: user:*)',
|
||||
searchHistory: '검색 기록',
|
||||
clearHistory: '기록 지우기',
|
||||
noHistory: '기록 없음',
|
||||
cancelScan: '스캔 취소',
|
||||
scanCancelled: '스캔이 취소되었습니다',
|
||||
switchTree: '트리 보기로 전환',
|
||||
switchList: '목록 보기로 전환',
|
||||
exportKeys: '키 내보내기',
|
||||
importKeys: '키 가져오기',
|
||||
exportDone: '{n}개 키를 내보냈습니다',
|
||||
importDone: '{n}개 키를 가져왔습니다',
|
||||
importFailed: '가져오기 실패: {error}',
|
||||
binaryKey: '바이너리 키',
|
||||
copyHex: 'Hex 복사',
|
||||
copyKeyName: '키 이름 복사',
|
||||
copyValue: '값 복사',
|
||||
memoryUsage: '메모리 사용량',
|
||||
memoryUsageResult: '메모리 사용량: {size} 바이트',
|
||||
selectAll: '모두 선택',
|
||||
},
|
||||
types: {
|
||||
string: 'STRING',
|
||||
hash: 'HASH',
|
||||
list: 'LIST',
|
||||
set: 'SET',
|
||||
zset: 'ZSET',
|
||||
stream: 'STREAM',
|
||||
},
|
||||
editor: {
|
||||
save: '저장',
|
||||
cancel: '취소',
|
||||
edit: '편집',
|
||||
saved: '저장됨',
|
||||
push: '푸시',
|
||||
addField: '필드 추가',
|
||||
addMember: '멤버 추가',
|
||||
addEntry: '항목 추가',
|
||||
filter: '필터...',
|
||||
refresh: '새로고침',
|
||||
elements: '{n}개 요소',
|
||||
members: '{n}개 멤버',
|
||||
empty: '비어 있음',
|
||||
field: '필드',
|
||||
value: '값',
|
||||
score: '점수',
|
||||
member: '멤버',
|
||||
remove: '제거',
|
||||
removed: '제거됨',
|
||||
added: '추가됨',
|
||||
trim: '트림',
|
||||
trimConfirm: '스트림을 트림하시겠습니까?',
|
||||
entryId: 'ID (* 사용 시 자동 생성)',
|
||||
fields: '필드',
|
||||
maxLength: '최대 길이',
|
||||
format: '형식',
|
||||
formatAuto: '자동 감지',
|
||||
formatText: '텍스트',
|
||||
formatHex: 'Hex',
|
||||
formatJson: 'JSON',
|
||||
formatBinary: '바이너리',
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
formatCustom: '사용자 정의',
|
||||
customCommand: '사용자 정의 명령',
|
||||
customCommandHint: '템플릿: {KEY} {VALUE} {HEX_FILE}',
|
||||
consumerGroups: '컨슈머 그룹',
|
||||
group: '그룹',
|
||||
consumers: '컨슈머',
|
||||
pending: '보류 중',
|
||||
createGroup: '그룹 생성',
|
||||
destroyGroup: '그룹 삭제',
|
||||
groupName: '그룹 이름',
|
||||
startId: '시작 ID',
|
||||
mkstream: '스트림 자동 생성',
|
||||
ack: 'ACK',
|
||||
ackConfirm: '선택한 항목을 ACK하시겠습니까?',
|
||||
noGroups: '컨슈머 그룹이 없습니다',
|
||||
valueTooLarge: '값이 너무 커서 표시할 수 없습니다 (>20MB)',
|
||||
download: '다운로드',
|
||||
setFieldTtl: '필드 TTL 설정',
|
||||
fieldTtl: '필드 TTL',
|
||||
sortAsc: '오름차순',
|
||||
sortDesc: '내림차순',
|
||||
filteredCount: '{n}개 필터링됨',
|
||||
fieldCount: '{n}개 필드',
|
||||
},
|
||||
cli: {
|
||||
title: 'CLI',
|
||||
welcome: 'Redis CLI - 아래에 명령어를 입력하세요',
|
||||
placeholder: 'Redis 명령어 입력...',
|
||||
stopSubscribe: '구독 중지',
|
||||
stopMonitor: '모니터 중지',
|
||||
subscribed: '구독 중, 메시지 대기 중...',
|
||||
monitoring: '모니터링 중, 명령어 대기 중...',
|
||||
subscribedTo: '구독 중: {channels}',
|
||||
txStarted: '트랜잭션 시작, 명령어가 대기열에 추가됩니다...',
|
||||
txQueued: '대기열에 추가됨',
|
||||
txExecuted: '트랜잭션 실행됨',
|
||||
txDiscarded: '트랜잭션 취소됨',
|
||||
txQueue: '대기열 ({n})',
|
||||
txPrompt: 'tx>',
|
||||
importCommands: '명령어 가져오기',
|
||||
importCmdDone: '{n}개 명령어를 실행했습니다',
|
||||
hotkeys: '단축키',
|
||||
hotkeysTitle: '키보드 단축키',
|
||||
},
|
||||
slowlog: {
|
||||
title: '슬로우 로그',
|
||||
time: '시간',
|
||||
duration: '소요 시간',
|
||||
command: '명령어',
|
||||
threshold: '임계값',
|
||||
maxLen: '최대 길이',
|
||||
},
|
||||
memory: {
|
||||
title: '메모리 분석',
|
||||
scan: '스캔 시작',
|
||||
pause: '일시 정지',
|
||||
resume: '재개',
|
||||
stop: '중지',
|
||||
scanning: '스캔 중...',
|
||||
paused: '일시 정지됨',
|
||||
noData: '데이터 없음',
|
||||
key: '키',
|
||||
size: '크기',
|
||||
type: '유형',
|
||||
totalSize: '총 크기',
|
||||
minSize: '최소 크기 (KB)',
|
||||
sortAsc: '오름차순',
|
||||
sortDesc: '내림차순',
|
||||
scanned: '{n}개 키 스캔됨',
|
||||
},
|
||||
commandLog: {
|
||||
title: '명령어 로그',
|
||||
time: '시간',
|
||||
command: '명령어',
|
||||
duration: '소요 시간',
|
||||
connection: '연결',
|
||||
onlyWrite: '쓰기 전용',
|
||||
clear: '지우기',
|
||||
empty: '기록된 명령어가 없습니다',
|
||||
},
|
||||
updater: {
|
||||
checking: '업데이트 확인 중...',
|
||||
available: '새 버전 {version} 사용 가능',
|
||||
notAvailable: '최신 버전입니다',
|
||||
downloading: '다운로드 중... {percent}%',
|
||||
downloaded: '업데이트가 다운로드되었습니다. 지금 다시 시작하시겠습니까?',
|
||||
install: '지금 다시 시작',
|
||||
later: '나중에',
|
||||
error: '업데이트 실패: {message}',
|
||||
},
|
||||
status: {
|
||||
title: '상태',
|
||||
database: '데이터베이스',
|
||||
keys: '키 수',
|
||||
refresh: '새로고침',
|
||||
flushDb: 'DB 플러시',
|
||||
flushDbConfirm: '현재 데이터베이스의 모든 키가 삭제됩니다! 확인하려면 FLUSHDB를 입력하세요:',
|
||||
flushDbDone: '데이터베이스가 플러시되었습니다',
|
||||
connecting: '연결 중...',
|
||||
error: '오류: {error}',
|
||||
noConnection: '연결 없음',
|
||||
autoRefresh: '자동 새로고침',
|
||||
searchInfo: 'INFO 검색...',
|
||||
noResults: '결과 없음',
|
||||
keyStats: '키 통계',
|
||||
dbColumn: '데이터베이스',
|
||||
keysColumn: '키 수',
|
||||
expiresColumn: '만료',
|
||||
avgTtlColumn: '평균 TTL',
|
||||
sectionServer: '서버',
|
||||
sectionClients: '클라이언트',
|
||||
sectionMemory: '메모리',
|
||||
sectionPersistence: '지속성',
|
||||
sectionStats: '통계',
|
||||
sectionReplication: '복제',
|
||||
sectionCpu: 'CPU',
|
||||
sectionModules: '모듈',
|
||||
sectionErrorstats: '오류 통계',
|
||||
sectionCluster: '클러스터',
|
||||
sectionKeyspace: '키스페이스',
|
||||
},
|
||||
db: {
|
||||
label: 'DB',
|
||||
keyCount: '{n}개 키',
|
||||
noKeys: '비어 있음',
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
theme: '테마',
|
||||
themeDark: '다크',
|
||||
themeLight: '라이트',
|
||||
themeSystem: '시스템',
|
||||
language: '언어',
|
||||
scanCount: 'SCAN당 키 수',
|
||||
fontSize: '글꼴 크기',
|
||||
zoom: '페이지 확대/축소',
|
||||
fontFamily: '고정폭 글꼴',
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user