feat(ui): add session sidebar search and bulk selection
Adds an optional session filter bar to the left sidebar with title search across parent/child sessions and a scoped Select All. Introduces multi-select checkboxes, bulk delete with clear selection controls, and confirmation dialogs for both single and bulk deletions using the existing alert dialog flow. Updates session i18n strings across supported locales.
This commit is contained in:
@@ -911,6 +911,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
enableFilterBar
|
||||
showHeader={false}
|
||||
showFooter={false}
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCl
|
||||
import type { SessionStatus } from "../types/session"
|
||||
import type { SessionThread } from "../stores/session-state"
|
||||
import { getSessionStatus } from "../stores/session-status"
|
||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare } from "lucide-solid"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import SessionRenameDialog from "./session-rename-dialog"
|
||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { showConfirmDialog } from "../stores/alerts"
|
||||
import {
|
||||
deleteSession,
|
||||
ensureSessionParentExpanded,
|
||||
@@ -35,6 +36,7 @@ interface SessionListProps {
|
||||
showFooter?: boolean
|
||||
headerContent?: JSX.Element
|
||||
footerContent?: JSX.Element
|
||||
enableFilterBar?: boolean
|
||||
}
|
||||
|
||||
function formatSessionStatus(status: SessionStatus): string {
|
||||
@@ -46,6 +48,70 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
||||
const [isRenaming, setIsRenaming] = createSignal(false)
|
||||
|
||||
const [filterQuery, setFilterQuery] = createSignal("")
|
||||
const normalizedQuery = createMemo(() => filterQuery().trim().toLowerCase())
|
||||
|
||||
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
||||
|
||||
const normalizeSessionLabel = (sessionId: string) => {
|
||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||
const title = (session?.title ?? "").trim()
|
||||
return title || t("sessionList.session.untitled")
|
||||
}
|
||||
|
||||
const sessionMatchesQuery = (sessionId: string, query: string) => {
|
||||
if (!query) return true
|
||||
const label = normalizeSessionLabel(sessionId).toLowerCase()
|
||||
if (label.includes(query)) return true
|
||||
return sessionId.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
const filteredThreads = createMemo<SessionThread[]>(() => {
|
||||
const query = normalizedQuery()
|
||||
if (!query) return props.threads
|
||||
|
||||
const next: SessionThread[] = []
|
||||
for (const thread of props.threads) {
|
||||
const parentMatches = sessionMatchesQuery(thread.parent.id, query)
|
||||
const matchingChildren = thread.children.filter((child) => sessionMatchesQuery(child.id, query))
|
||||
|
||||
if (!parentMatches && matchingChildren.length === 0) continue
|
||||
|
||||
next.push({
|
||||
parent: thread.parent,
|
||||
children: matchingChildren,
|
||||
latestUpdated: thread.latestUpdated,
|
||||
})
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
|
||||
const allMatchingSessionIds = createMemo<string[]>(() => {
|
||||
const ids: string[] = []
|
||||
for (const thread of filteredThreads()) {
|
||||
ids.push(thread.parent.id)
|
||||
for (const child of thread.children) ids.push(child.id)
|
||||
}
|
||||
return ids
|
||||
})
|
||||
|
||||
const selectedCount = createMemo(() => selectedSessionIds().size)
|
||||
|
||||
const isAllSelected = createMemo(() => {
|
||||
const ids = allMatchingSessionIds()
|
||||
if (ids.length === 0) return false
|
||||
const selected = selectedSessionIds()
|
||||
return ids.every((id) => selected.has(id))
|
||||
})
|
||||
const isSelectAllIndeterminate = createMemo(() => {
|
||||
const ids = allMatchingSessionIds()
|
||||
const total = ids.length
|
||||
if (total === 0) return false
|
||||
const count = selectedCount()
|
||||
return count > 0 && count < total
|
||||
})
|
||||
|
||||
const isSessionDeleting = (sessionId: string) => {
|
||||
const deleting = loading().deletingSession.get(props.instanceId)
|
||||
return deleting ? deleting.has(sessionId) : false
|
||||
@@ -82,6 +148,17 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
event.stopPropagation()
|
||||
if (isSessionDeleting(sessionId)) return
|
||||
|
||||
const confirmed = await showConfirmDialog(
|
||||
t("sessionList.delete.confirmMessage", { label: normalizeSessionLabel(sessionId) }),
|
||||
{
|
||||
title: t("sessionList.delete.title"),
|
||||
variant: "warning",
|
||||
confirmLabel: t("sessionList.delete.confirmLabel"),
|
||||
cancelLabel: t("sessionList.delete.cancelLabel"),
|
||||
},
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
const shouldSelectFallback = props.activeSessionId === sessionId
|
||||
let fallbackSessionId: string | undefined
|
||||
|
||||
@@ -152,6 +229,115 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
const setSelectedMany = (sessionIds: string[], checked: boolean) => {
|
||||
if (sessionIds.length === 0) return
|
||||
setSelectedSessionIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
sessionIds.forEach((id) => {
|
||||
if (checked) next.add(id)
|
||||
else next.delete(id)
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectableThreadIds = (parentId: string): string[] => {
|
||||
const query = normalizedQuery()
|
||||
const source = query ? filteredThreads() : props.threads
|
||||
const thread = source.find((t) => t.parent.id === parentId)
|
||||
if (!thread) return [parentId]
|
||||
return [thread.parent.id, ...thread.children.map((c) => c.id)]
|
||||
}
|
||||
|
||||
const getAllSessionIdsInOrder = (threads: SessionThread[]): string[] => {
|
||||
const ids: string[] = []
|
||||
threads.forEach((thread) => {
|
||||
ids.push(thread.parent.id)
|
||||
thread.children.forEach((child) => ids.push(child.id))
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
const handleToggleSelectAll = (checked: boolean) => {
|
||||
const ids = allMatchingSessionIds()
|
||||
setSelectedMany(ids, checked)
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (isAllSelected()) {
|
||||
handleToggleSelectAll(false)
|
||||
return
|
||||
}
|
||||
handleToggleSelectAll(true)
|
||||
}
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
const selected = Array.from(selectedSessionIds())
|
||||
if (selected.length === 0) return
|
||||
|
||||
const confirmed = await showConfirmDialog(
|
||||
t("sessionList.bulkDelete.confirmMessage", { count: selected.length }),
|
||||
{
|
||||
title: t("sessionList.bulkDelete.title"),
|
||||
variant: "warning",
|
||||
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
|
||||
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
|
||||
},
|
||||
)
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
const deletedSet = new Set(selected)
|
||||
const currentActiveId = props.activeSessionId
|
||||
|
||||
let fallbackSessionId: string | undefined
|
||||
if (currentActiveId && deletedSet.has(currentActiveId)) {
|
||||
const ordered = getAllSessionIdsInOrder(props.threads)
|
||||
const currentIndex = ordered.indexOf(currentActiveId)
|
||||
|
||||
for (let i = Math.max(0, currentIndex); i < ordered.length; i++) {
|
||||
const candidate = ordered[i]
|
||||
if (candidate && !deletedSet.has(candidate)) {
|
||||
fallbackSessionId = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!fallbackSessionId) {
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const candidate = ordered[i]
|
||||
if (candidate && !deletedSet.has(candidate)) {
|
||||
fallbackSessionId = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let failed = 0
|
||||
for (const sessionId of selected) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deleteSession(props.instanceId, sessionId)
|
||||
} catch (error) {
|
||||
failed += 1
|
||||
log.error(`Failed to delete session ${sessionId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedSessionIds(new Set<string>())
|
||||
|
||||
if (fallbackSessionId) {
|
||||
setActiveSessionFromList(props.instanceId, fallbackSessionId)
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
showToastNotification({
|
||||
message: t("sessionList.bulkDelete.error", { count: failed }),
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const SessionRow: Component<{
|
||||
@@ -190,9 +376,31 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
? t("sessionList.status.needsInput")
|
||||
: statusLabel()
|
||||
|
||||
return (
|
||||
<div class="session-list-item group">
|
||||
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
||||
|
||||
const parentGroupState = createMemo(() => {
|
||||
if (rowProps.isChild) {
|
||||
return { checked: isSelected(), indeterminate: false, ids: [rowProps.sessionId] }
|
||||
}
|
||||
|
||||
const ids = getSelectableThreadIds(rowProps.sessionId)
|
||||
const selected = selectedSessionIds()
|
||||
const selectedInGroup = ids.reduce((count, id) => (selected.has(id) ? count + 1 : count), 0)
|
||||
return {
|
||||
checked: selectedInGroup > 0 && selectedInGroup === ids.length,
|
||||
indeterminate: selectedInGroup > 0 && selectedInGroup < ids.length,
|
||||
ids,
|
||||
}
|
||||
})
|
||||
|
||||
let rowCheckboxEl: HTMLInputElement | null = null
|
||||
createEffect(() => {
|
||||
if (!rowCheckboxEl) return
|
||||
rowCheckboxEl.indeterminate = parentGroupState().indeterminate
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||
data-session-id={rowProps.sessionId}
|
||||
@@ -204,11 +412,23 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
>
|
||||
<div class="session-item-row session-item-header">
|
||||
<div class="session-item-title-row">
|
||||
{rowProps.isChild ? (
|
||||
<Bot class="w-4 h-4 flex-shrink-0" />
|
||||
) : (
|
||||
<User class="w-4 h-4 flex-shrink-0" />
|
||||
)}
|
||||
<Show when={props.enableFilterBar}>
|
||||
<input
|
||||
ref={(el) => {
|
||||
rowCheckboxEl = el
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={parentGroupState().checked}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
setSelectedMany(parentGroupState().ids, event.currentTarget.checked)
|
||||
}}
|
||||
aria-label={t("sessionList.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
|
||||
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,9 +436,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Show
|
||||
when={rowProps.hasChildren && !rowProps.isChild}
|
||||
fallback={
|
||||
rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />
|
||||
}
|
||||
fallback={rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />}
|
||||
>
|
||||
<span
|
||||
class={`session-item-expander opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||
@@ -228,18 +446,16 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")}
|
||||
aria-label={
|
||||
rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")
|
||||
}
|
||||
title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")}
|
||||
>
|
||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||
</span>
|
||||
</Show>
|
||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
|
||||
{needsInput() ? (
|
||||
<ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<span class="status-dot" />
|
||||
)}
|
||||
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||
{statusText()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -365,6 +581,63 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
<div
|
||||
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
||||
>
|
||||
<Show when={props.enableFilterBar}>
|
||||
<div class="p-3 border-b border-base">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted" aria-hidden="true">
|
||||
<Search class="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input pl-9"
|
||||
value={filterQuery()}
|
||||
onInput={(e) => setFilterQuery(e.currentTarget.value)}
|
||||
placeholder={t("sessionList.filter.placeholder")}
|
||||
aria-label={t("sessionList.filter.ariaLabel")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary p-2 inline-flex items-center justify-center"
|
||||
onClick={toggleSelectAll}
|
||||
disabled={allMatchingSessionIds().length === 0}
|
||||
aria-label={t("sessionList.selection.selectAllAriaLabel")}
|
||||
title={t("sessionList.selection.selectAllLabel")}
|
||||
>
|
||||
<Show
|
||||
when={isSelectAllIndeterminate()}
|
||||
fallback={isAllSelected() ? <CheckSquare class="w-4 h-4" /> : <Square class="w-4 h-4" />}
|
||||
>
|
||||
<MinusSquare class="w-4 h-4" />
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={selectedCount() > 0}>
|
||||
<div class="mt-2 flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary"
|
||||
onClick={handleBulkDelete}
|
||||
aria-label={t("sessionList.bulkDelete.ariaLabel", { count: selectedCount() })}
|
||||
>
|
||||
{t("sessionList.bulkDelete.button", { count: selectedCount() })}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary"
|
||||
onClick={() => setSelectedSessionIds(new Set<string>())}
|
||||
aria-label={t("sessionList.selection.clearAriaLabel")}
|
||||
>
|
||||
{t("sessionList.selection.clearLabel")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showHeader !== false}>
|
||||
<div class="session-list-header p-3 border-b border-base">
|
||||
{props.headerContent ?? (
|
||||
@@ -378,33 +651,33 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
|
||||
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
|
||||
|
||||
<Show when={props.threads.length > 0}>
|
||||
<div class="session-section">
|
||||
<For each={props.threads}>
|
||||
<Show when={filteredThreads().length > 0}>
|
||||
<div class="session-section">
|
||||
<For each={filteredThreads()}>
|
||||
|
||||
{(thread) => {
|
||||
const expanded = () => isSessionParentExpanded(props.instanceId, thread.parent.id)
|
||||
return (
|
||||
<>
|
||||
<SessionRow
|
||||
sessionId={thread.parent.id}
|
||||
hasChildren={thread.children.length > 0}
|
||||
expanded={expanded()}
|
||||
onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)}
|
||||
/>
|
||||
{(thread) => {
|
||||
const expanded = () => (normalizedQuery() ? true : isSessionParentExpanded(props.instanceId, thread.parent.id))
|
||||
return (
|
||||
<>
|
||||
<SessionRow
|
||||
sessionId={thread.parent.id}
|
||||
hasChildren={thread.children.length > 0}
|
||||
expanded={expanded()}
|
||||
onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)}
|
||||
/>
|
||||
|
||||
<Show when={expanded() && thread.children.length > 0}>
|
||||
<For each={thread.children}>
|
||||
{(child, index) => (
|
||||
<SessionRow sessionId={child.id} isChild isLastChild={index() === thread.children.length - 1} />
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
<Show when={expanded() && thread.children.length > 0}>
|
||||
<For each={thread.children}>
|
||||
{(child, index) => (
|
||||
<SessionRow sessionId={child.id} isChild isLastChild={index() === thread.children.length - 1} />
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "Session ID copied",
|
||||
"sessionList.copyId.error": "Unable to copy session ID",
|
||||
"sessionList.delete.error": "Unable to delete session",
|
||||
"sessionList.delete.title": "Delete session",
|
||||
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
||||
"sessionList.delete.confirmLabel": "Delete",
|
||||
"sessionList.delete.cancelLabel": "Cancel",
|
||||
"sessionList.rename.error": "Unable to rename session",
|
||||
|
||||
"sessionList.filter.placeholder": "Search sessions…",
|
||||
"sessionList.filter.ariaLabel": "Search sessions",
|
||||
"sessionList.selection.selectAllLabel": "Select all",
|
||||
"sessionList.selection.selectAllAriaLabel": "Select all sessions",
|
||||
"sessionList.selection.clearLabel": "Clear",
|
||||
"sessionList.selection.clearAriaLabel": "Clear selection",
|
||||
"sessionList.selection.checkboxAriaLabel": "Select session",
|
||||
"sessionList.bulkDelete.button": "Delete {count}",
|
||||
"sessionList.bulkDelete.ariaLabel": "Delete {count} selected sessions",
|
||||
"sessionList.bulkDelete.title": "Delete sessions",
|
||||
"sessionList.bulkDelete.confirmMessage": "Delete {count} selected sessions? This cannot be undone.",
|
||||
"sessionList.bulkDelete.confirmLabel": "Delete",
|
||||
"sessionList.bulkDelete.cancelLabel": "Cancel",
|
||||
"sessionList.bulkDelete.error": "Unable to delete {count} sessions",
|
||||
|
||||
"sessionRenameDialog.title": "Rename Session",
|
||||
"sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".",
|
||||
"sessionRenameDialog.description.default": "Set a new title for this session.",
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "ID de sesión copiado",
|
||||
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
||||
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
||||
"sessionList.delete.title": "Eliminar sesión",
|
||||
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
||||
"sessionList.delete.confirmLabel": "Eliminar",
|
||||
"sessionList.delete.cancelLabel": "Cancelar",
|
||||
"sessionList.rename.error": "No se pudo renombrar la sesión",
|
||||
|
||||
"sessionList.filter.placeholder": "Buscar sesiones…",
|
||||
"sessionList.filter.ariaLabel": "Buscar sesiones",
|
||||
"sessionList.selection.selectAllLabel": "Seleccionar todo",
|
||||
"sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones",
|
||||
"sessionList.selection.clearLabel": "Limpiar",
|
||||
"sessionList.selection.clearAriaLabel": "Limpiar selección",
|
||||
"sessionList.selection.checkboxAriaLabel": "Seleccionar sesión",
|
||||
"sessionList.bulkDelete.button": "Eliminar {count}",
|
||||
"sessionList.bulkDelete.ariaLabel": "Eliminar {count} sesiones seleccionadas",
|
||||
"sessionList.bulkDelete.title": "Eliminar sesiones",
|
||||
"sessionList.bulkDelete.confirmMessage": "¿Eliminar {count} sesiones seleccionadas? Esto no se puede deshacer.",
|
||||
"sessionList.bulkDelete.confirmLabel": "Eliminar",
|
||||
"sessionList.bulkDelete.cancelLabel": "Cancelar",
|
||||
"sessionList.bulkDelete.error": "No se pudieron eliminar {count} sesiones",
|
||||
|
||||
"sessionRenameDialog.title": "Renombrar sesión",
|
||||
"sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".",
|
||||
"sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.",
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "ID de session copié",
|
||||
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
||||
"sessionList.delete.error": "Impossible de supprimer la session",
|
||||
"sessionList.delete.title": "Supprimer la session",
|
||||
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
||||
"sessionList.delete.confirmLabel": "Supprimer",
|
||||
"sessionList.delete.cancelLabel": "Annuler",
|
||||
"sessionList.rename.error": "Impossible de renommer la session",
|
||||
|
||||
"sessionList.filter.placeholder": "Rechercher des sessions…",
|
||||
"sessionList.filter.ariaLabel": "Rechercher des sessions",
|
||||
"sessionList.selection.selectAllLabel": "Tout sélectionner",
|
||||
"sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions",
|
||||
"sessionList.selection.clearLabel": "Effacer",
|
||||
"sessionList.selection.clearAriaLabel": "Effacer la sélection",
|
||||
"sessionList.selection.checkboxAriaLabel": "Sélectionner la session",
|
||||
"sessionList.bulkDelete.button": "Supprimer {count}",
|
||||
"sessionList.bulkDelete.ariaLabel": "Supprimer {count} sessions sélectionnées",
|
||||
"sessionList.bulkDelete.title": "Supprimer des sessions",
|
||||
"sessionList.bulkDelete.confirmMessage": "Supprimer {count} sessions sélectionnées ? Cette action est irréversible.",
|
||||
"sessionList.bulkDelete.confirmLabel": "Supprimer",
|
||||
"sessionList.bulkDelete.cancelLabel": "Annuler",
|
||||
"sessionList.bulkDelete.error": "Impossible de supprimer {count} sessions",
|
||||
|
||||
"sessionRenameDialog.title": "Renommer la session",
|
||||
"sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".",
|
||||
"sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.",
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "セッション ID をコピーしました",
|
||||
"sessionList.copyId.error": "セッション ID をコピーできません",
|
||||
"sessionList.delete.error": "セッションを削除できません",
|
||||
"sessionList.delete.title": "セッションを削除",
|
||||
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
||||
"sessionList.delete.confirmLabel": "削除",
|
||||
"sessionList.delete.cancelLabel": "キャンセル",
|
||||
"sessionList.rename.error": "セッション名を変更できません",
|
||||
|
||||
"sessionList.filter.placeholder": "セッションを検索…",
|
||||
"sessionList.filter.ariaLabel": "セッションを検索",
|
||||
"sessionList.selection.selectAllLabel": "すべて選択",
|
||||
"sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択",
|
||||
"sessionList.selection.clearLabel": "クリア",
|
||||
"sessionList.selection.clearAriaLabel": "選択をクリア",
|
||||
"sessionList.selection.checkboxAriaLabel": "セッションを選択",
|
||||
"sessionList.bulkDelete.button": "{count} 件を削除",
|
||||
"sessionList.bulkDelete.ariaLabel": "選択した {count} 件のセッションを削除",
|
||||
"sessionList.bulkDelete.title": "セッションを削除",
|
||||
"sessionList.bulkDelete.confirmMessage": "選択した {count} 件のセッションを削除しますか?この操作は元に戻せません。",
|
||||
"sessionList.bulkDelete.confirmLabel": "削除",
|
||||
"sessionList.bulkDelete.cancelLabel": "キャンセル",
|
||||
"sessionList.bulkDelete.error": "{count} 件のセッションを削除できません",
|
||||
|
||||
"sessionRenameDialog.title": "セッション名を変更",
|
||||
"sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。",
|
||||
"sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。",
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "ID сессии скопирован",
|
||||
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
||||
"sessionList.delete.error": "Не удалось удалить сессию",
|
||||
"sessionList.delete.title": "Удалить сессию",
|
||||
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
||||
"sessionList.delete.confirmLabel": "Удалить",
|
||||
"sessionList.delete.cancelLabel": "Отмена",
|
||||
"sessionList.rename.error": "Не удалось переименовать сессию",
|
||||
|
||||
"sessionList.filter.placeholder": "Поиск сессий…",
|
||||
"sessionList.filter.ariaLabel": "Поиск сессий",
|
||||
"sessionList.selection.selectAllLabel": "Выбрать все",
|
||||
"sessionList.selection.selectAllAriaLabel": "Выбрать все сессии",
|
||||
"sessionList.selection.clearLabel": "Очистить",
|
||||
"sessionList.selection.clearAriaLabel": "Очистить выбор",
|
||||
"sessionList.selection.checkboxAriaLabel": "Выбрать сессию",
|
||||
"sessionList.bulkDelete.button": "Удалить {count}",
|
||||
"sessionList.bulkDelete.ariaLabel": "Удалить {count} выбранных сессий",
|
||||
"sessionList.bulkDelete.title": "Удалить сессии",
|
||||
"sessionList.bulkDelete.confirmMessage": "Удалить {count} выбранных сессий? Это действие нельзя отменить.",
|
||||
"sessionList.bulkDelete.confirmLabel": "Удалить",
|
||||
"sessionList.bulkDelete.cancelLabel": "Отмена",
|
||||
"sessionList.bulkDelete.error": "Не удалось удалить {count} сессий",
|
||||
|
||||
"sessionRenameDialog.title": "Переименовать сессию",
|
||||
"sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".",
|
||||
"sessionRenameDialog.description.default": "Установите новое название для этой сессии.",
|
||||
|
||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
||||
"sessionList.copyId.success": "已复制会话 ID",
|
||||
"sessionList.copyId.error": "无法复制会话 ID",
|
||||
"sessionList.delete.error": "无法删除会话",
|
||||
"sessionList.delete.title": "删除会话",
|
||||
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
||||
"sessionList.delete.confirmLabel": "删除",
|
||||
"sessionList.delete.cancelLabel": "取消",
|
||||
"sessionList.rename.error": "无法重命名会话",
|
||||
|
||||
"sessionList.filter.placeholder": "搜索会话…",
|
||||
"sessionList.filter.ariaLabel": "搜索会话",
|
||||
"sessionList.selection.selectAllLabel": "全选",
|
||||
"sessionList.selection.selectAllAriaLabel": "选择所有会话",
|
||||
"sessionList.selection.clearLabel": "清除",
|
||||
"sessionList.selection.clearAriaLabel": "清除选择",
|
||||
"sessionList.selection.checkboxAriaLabel": "选择会话",
|
||||
"sessionList.bulkDelete.button": "删除 {count}",
|
||||
"sessionList.bulkDelete.ariaLabel": "删除已选择的 {count} 个会话",
|
||||
"sessionList.bulkDelete.title": "删除会话",
|
||||
"sessionList.bulkDelete.confirmMessage": "删除已选择的 {count} 个会话?此操作无法撤销。",
|
||||
"sessionList.bulkDelete.confirmLabel": "删除",
|
||||
"sessionList.bulkDelete.cancelLabel": "取消",
|
||||
"sessionList.bulkDelete.error": "无法删除 {count} 个会话",
|
||||
|
||||
"sessionRenameDialog.title": "重命名会话",
|
||||
"sessionRenameDialog.description.withLabel": "更新“{label}”的标题。",
|
||||
"sessionRenameDialog.description.default": "为此会话设置新标题。",
|
||||
|
||||
Reference in New Issue
Block a user