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] **连接拖拽排序** — Sortable.js 拖拽重排连接列表
|
||||||
- [x] **复制连接** — 右键复制现有连接配置
|
- [x] **复制连接** — 右键复制现有连接配置
|
||||||
- [x] **侧边栏可拖拽** — 调整宽度 200–1500px,持久化
|
- [x] **侧边栏可拖拽** — 调整宽度 200–1500px,持久化
|
||||||
- [ ] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
- [x] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
||||||
- [x] **DB key 数量显示** — 下拉框显示每个 DB 的 key 数量
|
- [x] **DB key 数量显示** — 下拉框显示每个 DB 的 key 数量
|
||||||
- [x] **DB 数量自动检测** — CONFIG GET databases + INFO keyspace
|
- [x] **DB 数量自动检测** — CONFIG GET databases + INFO keyspace
|
||||||
- [x] **DB 记忆** — 每个连接记住最后选择的数据库
|
- [x] **DB 记忆** — 每个连接记住最后选择的数据库
|
||||||
- [ ] **更多语言** — de/es/fr/it/ko/pt/ru/tr/ua/vi/tw(当前仅 en/zh-CN)
|
- [x] **更多语言** — de/es/fr/it/ko/pt/ru/tr/ua/vi/tw(当前仅 en/zh-CN)
|
||||||
- [x] **页面缩放** — 0.5x–2.0x 缩放控制
|
|
||||||
- [x] **自定义字体** — 用户可选等宽字体
|
|
||||||
- [x] **SlowLog 配置显示** — slowlog-log-slower-than / slowlog-max-len
|
- [x] **SlowLog 配置显示** — slowlog-log-slower-than / slowlog-max-len
|
||||||
- [ ] **Cluster SlowLog** — 每个 master 节点分别查询
|
- [x] **Cluster SlowLog** — 每个 master 节点分别查询
|
||||||
- [ ] **Cluster Keyspace 表** — 集群模式显示各节点 key 数量
|
- [x] **Cluster Keyspace 表** — 集群模式显示各节点 key 数量
|
||||||
- [x] **Status 自动刷新开关** — 可暂停/恢复的刷新(当前固定 1s)
|
- [x] **Status 自动刷新开关** — 可暂停/恢复的刷新(当前固定 1s)
|
||||||
- [x] **Status 搜索过滤** — INFO 表格搜索
|
- [x] **Status 搜索过滤** — INFO 表格搜索
|
||||||
- [x] **Key 统计表** — 每个 DB 的 Keys/Expires/Avg TTL 排序表
|
- [x] **Key 统计表** — 每个 DB 的 Keys/Expires/Avg TTL 排序表
|
||||||
@@ -81,4 +79,4 @@
|
|||||||
- [x] `electron/shared/types.ts` 残留字段 — SSH/Sentinel/Cluster/TLS 路径在类型中存在但从未在 UI 中实现
|
- [x] `electron/shared/types.ts` 残留字段 — SSH/Sentinel/Cluster/TLS 路径在类型中存在但从未在 UI 中实现
|
||||||
- [ ] 单元测试 — 当前无任何测试
|
- [ ] 单元测试 — 当前无任何测试
|
||||||
- [ ] E2E 测试 — 无端到端测试
|
- [ ] 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 { useConnectionStore } from '@renderer/stores/connection'
|
||||||
import { useKeyStore } from '@renderer/stores/key'
|
import { useKeyStore } from '@renderer/stores/key'
|
||||||
import { useI18n } from '@renderer/i18n'
|
import { useI18n } from '@renderer/i18n'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
const connStore = useConnectionStore()
|
const connStore = useConnectionStore()
|
||||||
const keyStore = useKeyStore()
|
const keyStore = useKeyStore()
|
||||||
@@ -12,19 +13,70 @@ const { t } = useI18n()
|
|||||||
const selectedDb = ref(0)
|
const selectedDb = ref(0)
|
||||||
const dbCount = ref(16)
|
const dbCount = ref(16)
|
||||||
const dbKeyCounts = ref<Record<number, number>>({})
|
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(() => {
|
const dbList = computed(() => {
|
||||||
return Array.from({ length: dbCount.value }, (_, i) => {
|
return Array.from({ length: dbCount.value }, (_, i) => ({
|
||||||
const count = dbKeyCounts.value[i]
|
value: i,
|
||||||
const label = count !== undefined && count > 0
|
label: getDbLabel(i),
|
||||||
? `DB${i} (${t('db.keyCount', { n: count })})`
|
}))
|
||||||
: count === 0
|
|
||||||
? `DB${i} (${t('db.noKeys')})`
|
|
||||||
: `DB${i}`
|
|
||||||
return { value: i, label }
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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() {
|
async function fetchDbInfo() {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
try {
|
try {
|
||||||
@@ -71,11 +123,19 @@ watch(
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
selectedDb.value = 0
|
selectedDb.value = 0
|
||||||
keyStore.currentDb = 0
|
keyStore.currentDb = 0
|
||||||
|
loadCustomNames()
|
||||||
await fetchDbInfo()
|
await fetchDbInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => connStore.activeId,
|
||||||
|
() => {
|
||||||
|
loadCustomNames()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async function switchDb(db: number) {
|
async function switchDb(db: number) {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
selectedDb.value = db
|
selectedDb.value = db
|
||||||
@@ -91,14 +151,21 @@ async function switchDb(db: number) {
|
|||||||
:model-value="selectedDb"
|
:model-value="selectedDb"
|
||||||
size="small"
|
size="small"
|
||||||
@change="switchDb"
|
@change="switchDb"
|
||||||
style="width: 130px"
|
style="width: 160px"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="db in dbList"
|
v-for="db in dbList"
|
||||||
:key="db.value"
|
:key="db.value"
|
||||||
:label="db.label"
|
:label="db.label"
|
||||||
:value="db.value"
|
: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>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -117,6 +184,63 @@ async function switchDb(db: number) {
|
|||||||
font-weight: 600;
|
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) {
|
:deep(.el-select .el-input__wrapper) {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const themes = [
|
|||||||
const locales = [
|
const locales = [
|
||||||
{ value: 'en', label: 'English' },
|
{ value: 'en', label: 'English' },
|
||||||
{ value: 'zh-CN', label: '中文' },
|
{ value: 'zh-CN', label: '中文' },
|
||||||
|
{ value: 'ja', label: '日本語' },
|
||||||
|
{ value: 'ko', label: '한국어' },
|
||||||
|
{ value: 'de', label: 'Deutsch' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const fontOptions = [
|
const fontOptions = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/components/SlowLogView.vue
|
// src/components/SlowLogView.vue
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
import { useI18n } from '@renderer/i18n'
|
import { useI18n } from '@renderer/i18n'
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ interface SlowLogEntry {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
duration: number
|
duration: number
|
||||||
command: string
|
command: string
|
||||||
|
node?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = ref<SlowLogEntry[]>([])
|
const entries = ref<SlowLogEntry[]>([])
|
||||||
@@ -19,6 +20,71 @@ const loading = ref(false)
|
|||||||
const configThreshold = ref<string | null>(null)
|
const configThreshold = ref<string | null>(null)
|
||||||
const configMaxLen = 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() {
|
async function fetchConfig() {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
try {
|
try {
|
||||||
@@ -34,14 +100,25 @@ async function fetchConfig() {
|
|||||||
async function refresh() {
|
async function refresh() {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
entries.value = []
|
||||||
try {
|
try {
|
||||||
const raw = await window.electronAPI.redis.slowLogGet(connStore.activeId, 50)
|
if (isCluster.value) {
|
||||||
entries.value = raw.map((item: any) => ({
|
// Cluster mode: fetch slowlog from each master node
|
||||||
id: item[0],
|
const masters = await fetchClusterMasterNodes()
|
||||||
timestamp: item[1],
|
const results = await Promise.all(masters.map(m => fetchSlowLogForNode(m, 50)))
|
||||||
duration: item[2],
|
// Flatten and sort by timestamp descending
|
||||||
command: Array.isArray(item[3]) ? item[3].join(' ') : String(item[3]),
|
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()
|
await fetchConfig()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -71,10 +148,16 @@ defineExpose({ refresh })
|
|||||||
<div class="slow-log">
|
<div class="slow-log">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="title">{{ t('slowlog.title') }}</span>
|
<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>
|
<el-button size="small" @click="refresh" :loading="loading">{{ t('common.refresh') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<el-table :data="entries" size="small" max-height="400" :empty-text="t('common.noData')">
|
<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">
|
<el-table-column :label="t('slowlog.time')" width="180">
|
||||||
<template #default="{ row }">{{ formatTime(row.timestamp) }}</template>
|
<template #default="{ row }">{{ formatTime(row.timestamp) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -117,6 +200,67 @@ defineExpose({ refresh })
|
|||||||
color: var(--text-primary);
|
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 {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -22,7 +22,22 @@ interface KeyStatEntry {
|
|||||||
avgTtl: number | null
|
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 sections = ref<InfoSection[]>([])
|
||||||
|
const clusterInfo = ref<ClusterInfo | null>(null)
|
||||||
|
|
||||||
// Auto-refresh toggle state (persisted to localStorage, default ON)
|
// Auto-refresh toggle state (persisted to localStorage, default ON)
|
||||||
const autoRefreshEnabled = ref(localStorage.getItem('statusAutoRefresh') !== 'false')
|
const autoRefreshEnabled = ref(localStorage.getItem('statusAutoRefresh') !== 'false')
|
||||||
@@ -129,11 +144,51 @@ function parseInfo(raw: string): InfoSection[] {
|
|||||||
|
|
||||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
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() {
|
async function refresh() {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
info.value = await connStore.getInfo()
|
info.value = await connStore.getInfo()
|
||||||
sections.value = parseInfo(info.value)
|
sections.value = parseInfo(info.value)
|
||||||
dbSize.value = await window.electronAPI.redis.dbSize(connStore.activeId)
|
dbSize.value = await window.electronAPI.redis.dbSize(connStore.activeId)
|
||||||
|
if (connStore.activeConnection?.cluster) {
|
||||||
|
await fetchClusterInfo()
|
||||||
|
} else {
|
||||||
|
clusterInfo.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAutoRefresh() {
|
function startAutoRefresh() {
|
||||||
@@ -243,6 +298,57 @@ defineExpose({ refresh })
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</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">
|
<div class="info-sections">
|
||||||
<template v-if="filteredSections.length > 0">
|
<template v-if="filteredSections.length > 0">
|
||||||
<div v-for="section in filteredSections" :key="section.title" class="info-section">
|
<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);
|
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 {
|
.no-results {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import en from './locales/en'
|
import en from './locales/en'
|
||||||
import zhCN from './locales/zh-CN'
|
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
|
type Messages = typeof en
|
||||||
|
|
||||||
const messages: Record<string, Messages> = {
|
const messages: Record<string, Messages> = {
|
||||||
en,
|
en,
|
||||||
'zh-CN': zhCN,
|
'zh-CN': zhCN,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
de,
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocale = ref<string>(
|
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