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:
2026-07-07 23:07:14 +08:00
parent 71b2bd1771
commit 8a7d253180
9 changed files with 1383 additions and 26 deletions
+5 -7
View File
@@ -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.5x2.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
View File
@@ -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);
+3
View File
@@ -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 = [
+152 -8
View File
@@ -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;
+167
View File
@@ -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;
+6
View File
@@ -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>(
+305
View File
@@ -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',
},
}
+305
View File
@@ -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: '等幅フォント',
},
}
+305
View File
@@ -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: '고정폭 글꼴',
},
}