mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: clean up task list in HistoryPreview and History components (#6687)
Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Bruno Bergher <me@brunobergher.comexport> Co-authored-by: Bruno Bergher <me@brunobergher.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
parent
477b85de03
commit
24584f54ec
30 changed files with 567 additions and 220 deletions
|
|
@ -13,7 +13,7 @@ const VersionIndicator: React.FC<VersionIndicatorProps> = ({ onClick, className
|
|||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border border-vscode-panel-border hover:border-vscode-focusBorder ${className}`}
|
||||
className={`text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border ${className}`}
|
||||
aria-label={t("chat:versionIndicator.ariaLabel", { version: Package.version })}>
|
||||
v{Package.version}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
|
|||
size="icon"
|
||||
data-testid="delete-task-button"
|
||||
onClick={handleDeleteClick}
|
||||
className="group-hover:opacity-100 opacity-50 transition-opacity">
|
||||
className="opacity-70">
|
||||
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
</div>
|
||||
</TabHeader>
|
||||
|
||||
<TabContent className="p-0">
|
||||
<TabContent className="px-2 py-0">
|
||||
<Virtuoso
|
||||
className="flex-1 overflow-y-scroll"
|
||||
data={tasks}
|
||||
|
|
@ -243,7 +243,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
isSelected={selectedTaskIds.includes(item.id)}
|
||||
onToggleSelection={toggleTaskSelection}
|
||||
onDelete={setDeleteTaskId}
|
||||
className="m-2 mr-0"
|
||||
className="m-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -251,7 +251,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
|
||||
{/* Fixed action bar at bottom - only shown in selection mode with selected items */}
|
||||
{isSelectionMode && selectedTaskIds.length > 0 && (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-vscode-editor-background border-t border-vscode-panel-border p-2 flex justify-between items-center">
|
||||
<div className="fixed bottom-0 left-0 right-2 bg-vscode-editor-background border-t border-vscode-panel-border p-2 flex justify-between items-center">
|
||||
<div className="text-vscode-foreground">
|
||||
{t("history:selectedItems", { selected: selectedTaskIds.length, total: tasks.length })}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { vscode } from "@/utils/vscode"
|
|||
import { cn } from "@/lib/utils"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
|
||||
import TaskItemHeader from "./TaskItemHeader"
|
||||
import TaskItemFooter from "./TaskItemFooter"
|
||||
|
||||
interface DisplayHistoryItem extends HistoryItem {
|
||||
|
|
@ -48,11 +47,11 @@ const TaskItem = ({
|
|||
key={item.id}
|
||||
data-testid={`task-item-${item.id}`}
|
||||
className={cn(
|
||||
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden hover:border-vscode-toolbar-hoverBackground/60",
|
||||
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden border border-transparent hover:bg-vscode-list-hoverBackground transition-colors",
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}>
|
||||
<div className="flex gap-2 p-3">
|
||||
<div className={(!isCompact && isSelectionMode ? "pl-3 pb-3" : "pl-4") + " flex gap-3 px-3 pt-3 pb-1"}>
|
||||
{/* Selection checkbox - only in full variant */}
|
||||
{!isCompact && isSelectionMode && (
|
||||
<div
|
||||
|
|
@ -69,24 +68,25 @@ const TaskItem = ({
|
|||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Header with metadata */}
|
||||
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />
|
||||
|
||||
{/* Task content */}
|
||||
<div
|
||||
className={cn("overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis", {
|
||||
"text-base line-clamp-3": !isCompact,
|
||||
"line-clamp-2": isCompact,
|
||||
})}
|
||||
className={cn(
|
||||
"overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis line-clamp-2",
|
||||
{
|
||||
"text-base": !isCompact,
|
||||
},
|
||||
!isCompact && isSelectionMode ? "mb-1" : "",
|
||||
)}
|
||||
data-testid="task-content"
|
||||
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
|
||||
{item.highlight ? undefined : item.task}
|
||||
</div>
|
||||
<TaskItemFooter
|
||||
item={item}
|
||||
variant={variant}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
{/* Task Item Footer */}
|
||||
<TaskItemFooter item={item} variant={variant} isSelectionMode={isSelectionMode} />
|
||||
|
||||
{/* Workspace info */}
|
||||
{showWorkspace && item.workspace && (
|
||||
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
|
||||
<span className="codicon codicon-folder scale-80" />
|
||||
|
|
|
|||
|
|
@ -1,59 +1,41 @@
|
|||
import React from "react"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
import { Coins, FileIcon } from "lucide-react"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { formatTimeAgo } from "@/utils/format"
|
||||
import { CopyButton } from "./CopyButton"
|
||||
import { ExportButton } from "./ExportButton"
|
||||
import { DeleteButton } from "./DeleteButton"
|
||||
import { StandardTooltip } from "../ui/standard-tooltip"
|
||||
|
||||
export interface TaskItemFooterProps {
|
||||
item: HistoryItem
|
||||
variant: "compact" | "full"
|
||||
isSelectionMode?: boolean
|
||||
onDelete?: (taskId: string) => void
|
||||
}
|
||||
|
||||
const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false }) => {
|
||||
const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false, onDelete }) => {
|
||||
return (
|
||||
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center mt-1">
|
||||
<div className="flex gap-2">
|
||||
{!!(item.cacheReads || item.cacheWrites) && (
|
||||
<span className="flex items-center" data-testid="cache-compact">
|
||||
<i className="mr-1 codicon codicon-cloud-upload text-sm! text-vscode-descriptionForeground" />
|
||||
<span className="inline-block mr-1">{formatLargeNumber(item.cacheWrites || 0)}</span>
|
||||
<i className="mr-1 codicon codicon-cloud-download text-sm! text-vscode-descriptionForeground" />
|
||||
<span>{formatLargeNumber(item.cacheReads || 0)}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Full Tokens */}
|
||||
{!!(item.tokensIn || item.tokensOut) && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span data-testid="tokens-in-footer-compact">↑ {formatLargeNumber(item.tokensIn || 0)}</span>
|
||||
<span data-testid="tokens-out-footer-compact">↓ {formatLargeNumber(item.tokensOut || 0)}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Full Cost */}
|
||||
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center text-vscode-descriptionForeground/60">
|
||||
{/* Datetime with time-ago format */}
|
||||
<StandardTooltip content={new Date(item.ts).toLocaleString()}>
|
||||
<span className="first-letter:uppercase">{formatTimeAgo(item.ts)}</span>
|
||||
</StandardTooltip>
|
||||
<span>·</span>
|
||||
{/* Cost */}
|
||||
{!!item.totalCost && (
|
||||
<span className="flex items-center">
|
||||
<Coins className="inline-block size-[1em] mr-1" />
|
||||
<span data-testid="cost-footer-compact">{"$" + item.totalCost.toFixed(2)}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!!item.size && (
|
||||
<span className="flex items-center">
|
||||
<FileIcon className="inline-block size-[1em] mr-1" />
|
||||
<span data-testid="size-footer-compact">{prettyBytes(item.size)}</span>
|
||||
<span className="flex items-center" data-testid="cost-footer-compact">
|
||||
{"$" + item.totalCost.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons for non-compact view */}
|
||||
{!isSelectionMode && (
|
||||
<div className="flex flex-row gap-0 items-center opacity-50 hover:opacity-100">
|
||||
<div className="flex flex-row gap-0 items-center text-vscode-descriptionForeground/60 hover:text-vscode-descriptionForeground">
|
||||
<CopyButton itemTask={item.task} />
|
||||
{variant === "full" && <ExportButton itemId={item.id} />}
|
||||
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import React from "react"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
import { formatDate } from "@/utils/format"
|
||||
import { DeleteButton } from "./DeleteButton"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TaskItemHeaderProps {
|
||||
item: HistoryItem
|
||||
isSelectionMode: boolean
|
||||
onDelete?: (taskId: string) => void
|
||||
}
|
||||
|
||||
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex justify-between items-center", {
|
||||
// this is to balance out the margin when we don't have a delete button
|
||||
// because the delete button sorta pushes the date up due to its size
|
||||
"mb-1": !onDelete,
|
||||
})}>
|
||||
<div className="flex items-center flex-wrap gap-x-2 text-xs">
|
||||
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!isSelectionMode && (
|
||||
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
|
||||
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskItemHeader
|
||||
|
|
@ -148,6 +148,8 @@ describe("HistoryPreview", () => {
|
|||
expect(screen.getByTestId("task-item-task-2")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("task-item-task-3")).toBeInTheDocument()
|
||||
expect(screen.queryByTestId("task-item-task-4")).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId("task-item-task-5")).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId("task-item-task-6")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders only 1 task when there is only 1 task", () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ vi.mock("@src/i18n/TranslationContext", () => ({
|
|||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/utils/format", () => ({
|
||||
formatTimeAgo: vi.fn(() => "2 hours ago"),
|
||||
formatDate: vi.fn(() => "January 15 at 2:30 PM"),
|
||||
formatLargeNumber: vi.fn((num: number) => num.toString()),
|
||||
}))
|
||||
|
||||
const mockTask = {
|
||||
id: "1",
|
||||
number: 1,
|
||||
|
|
@ -74,16 +80,10 @@ describe("TaskItem", () => {
|
|||
expect(screen.getByTestId("export")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays cache information when present", () => {
|
||||
const mockTaskWithCache = {
|
||||
...mockTask,
|
||||
cacheReads: 10,
|
||||
cacheWrites: 5,
|
||||
}
|
||||
|
||||
it("displays time ago information", () => {
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTaskWithCache}
|
||||
item={mockTask}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={vi.fn()}
|
||||
|
|
@ -91,22 +91,14 @@ describe("TaskItem", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
// Should display cache information in the footer
|
||||
expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
|
||||
expect(screen.getByText("5")).toBeInTheDocument() // cache writes
|
||||
expect(screen.getByText("10")).toBeInTheDocument() // cache reads
|
||||
// Should display time ago format
|
||||
expect(screen.getByText(/ago/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not display cache information when not present", () => {
|
||||
const mockTaskWithoutCache = {
|
||||
...mockTask,
|
||||
cacheReads: 0,
|
||||
cacheWrites: 0,
|
||||
}
|
||||
|
||||
it("applies hover effect class", () => {
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTaskWithoutCache}
|
||||
item={mockTask}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={vi.fn()}
|
||||
|
|
@ -114,7 +106,7 @@ describe("TaskItem", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
// Cache section should not be present
|
||||
expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
|
||||
const taskItem = screen.getByTestId("task-item-1")
|
||||
expect(taskItem).toHaveClass("hover:bg-vscode-list-hoverBackground")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ vi.mock("@src/i18n/TranslationContext", () => ({
|
|||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/utils/format", () => ({
|
||||
formatTimeAgo: vi.fn(() => "2 hours ago"),
|
||||
formatDate: vi.fn(() => "January 15 at 2:30 PM"),
|
||||
formatLargeNumber: vi.fn((num: number) => num.toString()),
|
||||
}))
|
||||
|
||||
const mockItem = {
|
||||
id: "1",
|
||||
number: 1,
|
||||
|
|
@ -20,12 +26,11 @@ const mockItem = {
|
|||
}
|
||||
|
||||
describe("TaskItemFooter", () => {
|
||||
it("renders token information", () => {
|
||||
it("renders time ago information", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="full" />)
|
||||
|
||||
// Check for token counts using testids since the text is split across elements
|
||||
expect(screen.getByTestId("tokens-in-footer-compact")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("tokens-out-footer-compact")).toBeInTheDocument()
|
||||
// Should show time ago format
|
||||
expect(screen.getByText(/ago/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders cost information", () => {
|
||||
|
|
@ -43,31 +48,38 @@ describe("TaskItemFooter", () => {
|
|||
expect(screen.getByTestId("export")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders cache information when present", () => {
|
||||
const mockItemWithCache = {
|
||||
...mockItem,
|
||||
cacheReads: 5,
|
||||
cacheWrites: 3,
|
||||
}
|
||||
it("hides export button in compact variant", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="compact" />)
|
||||
|
||||
render(<TaskItemFooter item={mockItemWithCache} variant="full" />)
|
||||
|
||||
// Check for cache display using testid
|
||||
expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
|
||||
expect(screen.getByText("3")).toBeInTheDocument() // cache writes
|
||||
expect(screen.getByText("5")).toBeInTheDocument() // cache reads
|
||||
// Should show copy button but not export button
|
||||
expect(screen.getByTestId("copy-prompt-button")).toBeInTheDocument()
|
||||
expect(screen.queryByTestId("export")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render cache information when not present", () => {
|
||||
const mockItemWithoutCache = {
|
||||
...mockItem,
|
||||
cacheReads: 0,
|
||||
cacheWrites: 0,
|
||||
}
|
||||
it("hides action buttons in selection mode", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={true} />)
|
||||
|
||||
render(<TaskItemFooter item={mockItemWithoutCache} variant="full" />)
|
||||
// Should not show any action buttons
|
||||
expect(screen.queryByTestId("copy-prompt-button")).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId("export")).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Cache section should not be present
|
||||
expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
|
||||
it("shows delete button when not in selection mode and onDelete is provided", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={false} onDelete={vi.fn()} />)
|
||||
|
||||
expect(screen.getByTestId("delete-task-button")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not show delete button in selection mode", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={true} onDelete={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not show delete button when onDelete is not provided", () => {
|
||||
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={false} />)
|
||||
|
||||
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { render, screen } from "@/utils/test-utils"
|
||||
|
||||
import TaskItemHeader from "../TaskItemHeader"
|
||||
|
||||
vi.mock("@src/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockItem = {
|
||||
id: "1",
|
||||
number: 1,
|
||||
task: "Test task",
|
||||
ts: Date.now(),
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.002,
|
||||
workspace: "/test/workspace",
|
||||
}
|
||||
|
||||
describe("TaskItemHeader", () => {
|
||||
it("renders date information", () => {
|
||||
render(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={vi.fn()} />)
|
||||
|
||||
// TaskItemHeader shows the formatted date, not the task text
|
||||
expect(screen.getByText(/\w+ \d{1,2}, \d{1,2}:\d{2} \w{2}/)).toBeInTheDocument() // Date format like "JUNE 14, 10:15 AM"
|
||||
})
|
||||
|
||||
it("shows delete button when not in selection mode", () => {
|
||||
render(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
16
webview-ui/src/i18n/locales/ca/common.json
generated
16
webview-ui/src/i18n/locales/ca/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Editar missatge",
|
||||
"editWarning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
|
||||
"proceed": "Continuar"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "ara mateix",
|
||||
"seconds_ago": "fa {{count}} segons",
|
||||
"minute_ago": "fa un minut",
|
||||
"minutes_ago": "fa {{count}} minuts",
|
||||
"hour_ago": "fa una hora",
|
||||
"hours_ago": "fa {{count}} hores",
|
||||
"day_ago": "fa un dia",
|
||||
"days_ago": "fa {{count}} dies",
|
||||
"week_ago": "fa una setmana",
|
||||
"weeks_ago": "fa {{count}} setmanes",
|
||||
"month_ago": "fa un mes",
|
||||
"months_ago": "fa {{count}} mesos",
|
||||
"year_ago": "fa un any",
|
||||
"years_ago": "fa {{count}} anys"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/de/common.json
generated
16
webview-ui/src/i18n/locales/de/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Nachricht bearbeiten",
|
||||
"editWarning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
|
||||
"proceed": "Fortfahren"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "gerade eben",
|
||||
"seconds_ago": "vor {{count}} Sekunden",
|
||||
"minute_ago": "vor einer Minute",
|
||||
"minutes_ago": "vor {{count}} Minuten",
|
||||
"hour_ago": "vor einer Stunde",
|
||||
"hours_ago": "vor {{count}} Stunden",
|
||||
"day_ago": "vor einem Tag",
|
||||
"days_ago": "vor {{count}} Tagen",
|
||||
"week_ago": "vor einer Woche",
|
||||
"weeks_ago": "vor {{count}} Wochen",
|
||||
"month_ago": "vor einem Monat",
|
||||
"months_ago": "vor {{count}} Monaten",
|
||||
"year_ago": "vor einem Jahr",
|
||||
"years_ago": "vor {{count}} Jahren"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Edit Message",
|
||||
"editWarning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "just now",
|
||||
"seconds_ago": "{{count}} seconds ago",
|
||||
"minute_ago": "a minute ago",
|
||||
"minutes_ago": "{{count}} minutes ago",
|
||||
"hour_ago": "an hour ago",
|
||||
"hours_ago": "{{count}} hours ago",
|
||||
"day_ago": "a day ago",
|
||||
"days_ago": "{{count}} days ago",
|
||||
"week_ago": "a week ago",
|
||||
"weeks_ago": "{{count}} weeks ago",
|
||||
"month_ago": "a month ago",
|
||||
"months_ago": "{{count}} months ago",
|
||||
"year_ago": "a year ago",
|
||||
"years_ago": "{{count}} years ago"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/es/common.json
generated
16
webview-ui/src/i18n/locales/es/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Editar mensaje",
|
||||
"editWarning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
|
||||
"proceed": "Continuar"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "ahora mismo",
|
||||
"seconds_ago": "hace {{count}} segundos",
|
||||
"minute_ago": "hace un minuto",
|
||||
"minutes_ago": "hace {{count}} minutos",
|
||||
"hour_ago": "hace una hora",
|
||||
"hours_ago": "hace {{count}} horas",
|
||||
"day_ago": "hace un día",
|
||||
"days_ago": "hace {{count}} días",
|
||||
"week_ago": "hace una semana",
|
||||
"weeks_ago": "hace {{count}} semanas",
|
||||
"month_ago": "hace un mes",
|
||||
"months_ago": "hace {{count}} meses",
|
||||
"year_ago": "hace un año",
|
||||
"years_ago": "hace {{count}} años"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/fr/common.json
generated
16
webview-ui/src/i18n/locales/fr/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Modifier le message",
|
||||
"editWarning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
|
||||
"proceed": "Continuer"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "à l'instant",
|
||||
"seconds_ago": "il y a {{count}} secondes",
|
||||
"minute_ago": "il y a une minute",
|
||||
"minutes_ago": "il y a {{count}} minutes",
|
||||
"hour_ago": "il y a une heure",
|
||||
"hours_ago": "il y a {{count}} heures",
|
||||
"day_ago": "il y a un jour",
|
||||
"days_ago": "il y a {{count}} jours",
|
||||
"week_ago": "il y a une semaine",
|
||||
"weeks_ago": "il y a {{count}} semaines",
|
||||
"month_ago": "il y a un mois",
|
||||
"months_ago": "il y a {{count}} mois",
|
||||
"year_ago": "il y a un an",
|
||||
"years_ago": "il y a {{count}} ans"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/hi/common.json
generated
16
webview-ui/src/i18n/locales/hi/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "संदेश संपादित करें",
|
||||
"editWarning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"proceed": "जारी रखें"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "अभी",
|
||||
"seconds_ago": "{{count}} सेकंड पहले",
|
||||
"minute_ago": "एक मिनट पहले",
|
||||
"minutes_ago": "{{count}} मिनट पहले",
|
||||
"hour_ago": "एक घंटे पहले",
|
||||
"hours_ago": "{{count}} घंटे पहले",
|
||||
"day_ago": "एक दिन पहले",
|
||||
"days_ago": "{{count}} दिन पहले",
|
||||
"week_ago": "एक सप्ताह पहले",
|
||||
"weeks_ago": "{{count}} सप्ताह पहले",
|
||||
"month_ago": "एक महीने पहले",
|
||||
"months_ago": "{{count}} महीने पहले",
|
||||
"year_ago": "एक साल पहले",
|
||||
"years_ago": "{{count}} साल पहले"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/id/common.json
generated
16
webview-ui/src/i18n/locales/id/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Edit Pesan",
|
||||
"editWarning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
|
||||
"proceed": "Lanjutkan"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "baru saja",
|
||||
"seconds_ago": "{{count}} detik yang lalu",
|
||||
"minute_ago": "satu menit yang lalu",
|
||||
"minutes_ago": "{{count}} menit yang lalu",
|
||||
"hour_ago": "satu jam yang lalu",
|
||||
"hours_ago": "{{count}} jam yang lalu",
|
||||
"day_ago": "satu hari yang lalu",
|
||||
"days_ago": "{{count}} hari yang lalu",
|
||||
"week_ago": "satu minggu yang lalu",
|
||||
"weeks_ago": "{{count}} minggu yang lalu",
|
||||
"month_ago": "satu bulan yang lalu",
|
||||
"months_ago": "{{count}} bulan yang lalu",
|
||||
"year_ago": "satu tahun yang lalu",
|
||||
"years_ago": "{{count}} tahun yang lalu"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/it/common.json
generated
16
webview-ui/src/i18n/locales/it/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Modifica Messaggio",
|
||||
"editWarning": "Modificando questo messaggio verranno eliminati tutti i messaggi successivi nella conversazione. Vuoi procedere?",
|
||||
"proceed": "Procedi"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "proprio ora",
|
||||
"seconds_ago": "{{count}} secondi fa",
|
||||
"minute_ago": "un minuto fa",
|
||||
"minutes_ago": "{{count}} minuti fa",
|
||||
"hour_ago": "un'ora fa",
|
||||
"hours_ago": "{{count}} ore fa",
|
||||
"day_ago": "un giorno fa",
|
||||
"days_ago": "{{count}} giorni fa",
|
||||
"week_ago": "una settimana fa",
|
||||
"weeks_ago": "{{count}} settimane fa",
|
||||
"month_ago": "un mese fa",
|
||||
"months_ago": "{{count}} mesi fa",
|
||||
"year_ago": "un anno fa",
|
||||
"years_ago": "{{count}} anni fa"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ja/common.json
generated
16
webview-ui/src/i18n/locales/ja/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "メッセージを編集",
|
||||
"editWarning": "このメッセージを編集すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
|
||||
"proceed": "続行"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "たった今",
|
||||
"seconds_ago": "{{count}}秒前",
|
||||
"minute_ago": "1分前",
|
||||
"minutes_ago": "{{count}}分前",
|
||||
"hour_ago": "1時間前",
|
||||
"hours_ago": "{{count}}時間前",
|
||||
"day_ago": "1日前",
|
||||
"days_ago": "{{count}}日前",
|
||||
"week_ago": "1週間前",
|
||||
"weeks_ago": "{{count}}週間前",
|
||||
"month_ago": "1ヶ月前",
|
||||
"months_ago": "{{count}}ヶ月前",
|
||||
"year_ago": "1年前",
|
||||
"years_ago": "{{count}}年前"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ko/common.json
generated
16
webview-ui/src/i18n/locales/ko/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "메시지 편집",
|
||||
"editWarning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"proceed": "계속"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "방금",
|
||||
"seconds_ago": "{{count}}초 전",
|
||||
"minute_ago": "1분 전",
|
||||
"minutes_ago": "{{count}}분 전",
|
||||
"hour_ago": "1시간 전",
|
||||
"hours_ago": "{{count}}시간 전",
|
||||
"day_ago": "1일 전",
|
||||
"days_ago": "{{count}}일 전",
|
||||
"week_ago": "1주일 전",
|
||||
"weeks_ago": "{{count}}주일 전",
|
||||
"month_ago": "1개월 전",
|
||||
"months_ago": "{{count}}개월 전",
|
||||
"year_ago": "1년 전",
|
||||
"years_ago": "{{count}}년 전"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/nl/common.json
generated
16
webview-ui/src/i18n/locales/nl/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Bericht Bewerken",
|
||||
"editWarning": "Het bewerken van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
|
||||
"proceed": "Doorgaan"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "zojuist",
|
||||
"seconds_ago": "{{count}} seconden geleden",
|
||||
"minute_ago": "een minuut geleden",
|
||||
"minutes_ago": "{{count}} minuten geleden",
|
||||
"hour_ago": "een uur geleden",
|
||||
"hours_ago": "{{count}} uur geleden",
|
||||
"day_ago": "een dag geleden",
|
||||
"days_ago": "{{count}} dagen geleden",
|
||||
"week_ago": "een week geleden",
|
||||
"weeks_ago": "{{count}} weken geleden",
|
||||
"month_ago": "een maand geleden",
|
||||
"months_ago": "{{count}} maanden geleden",
|
||||
"year_ago": "een jaar geleden",
|
||||
"years_ago": "{{count}} jaar geleden"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/pl/common.json
generated
16
webview-ui/src/i18n/locales/pl/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Edytuj Wiadomość",
|
||||
"editWarning": "Edycja tej wiadomości spowoduje usunięcie wszystkich kolejnych wiadomości w rozmowie. Czy chcesz kontynuować?",
|
||||
"proceed": "Kontynuuj"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "przed chwilą",
|
||||
"seconds_ago": "{{count}} sekund temu",
|
||||
"minute_ago": "minutę temu",
|
||||
"minutes_ago": "{{count}} minut temu",
|
||||
"hour_ago": "godzinę temu",
|
||||
"hours_ago": "{{count}} godzin temu",
|
||||
"day_ago": "dzień temu",
|
||||
"days_ago": "{{count}} dni temu",
|
||||
"week_ago": "tydzień temu",
|
||||
"weeks_ago": "{{count}} tygodni temu",
|
||||
"month_ago": "miesiąc temu",
|
||||
"months_ago": "{{count}} miesięcy temu",
|
||||
"year_ago": "rok temu",
|
||||
"years_ago": "{{count}} lat temu"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/pt-BR/common.json
generated
16
webview-ui/src/i18n/locales/pt-BR/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Editar Mensagem",
|
||||
"editWarning": "Editar esta mensagem irá excluir todas as mensagens subsequentes na conversa. Deseja prosseguir?",
|
||||
"proceed": "Prosseguir"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "agora mesmo",
|
||||
"seconds_ago": "há {{count}} segundos",
|
||||
"minute_ago": "há um minuto",
|
||||
"minutes_ago": "há {{count}} minutos",
|
||||
"hour_ago": "há uma hora",
|
||||
"hours_ago": "há {{count}} horas",
|
||||
"day_ago": "há um dia",
|
||||
"days_ago": "há {{count}} dias",
|
||||
"week_ago": "há uma semana",
|
||||
"weeks_ago": "há {{count}} semanas",
|
||||
"month_ago": "há um mês",
|
||||
"months_ago": "há {{count}} meses",
|
||||
"year_ago": "há um ano",
|
||||
"years_ago": "há {{count}} anos"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ru/common.json
generated
16
webview-ui/src/i18n/locales/ru/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Редактировать Сообщение",
|
||||
"editWarning": "Редактирование этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
|
||||
"proceed": "Продолжить"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "только что",
|
||||
"seconds_ago": "{{count}} секунд назад",
|
||||
"minute_ago": "минуту назад",
|
||||
"minutes_ago": "{{count}} минут назад",
|
||||
"hour_ago": "час назад",
|
||||
"hours_ago": "{{count}} часов назад",
|
||||
"day_ago": "день назад",
|
||||
"days_ago": "{{count}} дней назад",
|
||||
"week_ago": "неделю назад",
|
||||
"weeks_ago": "{{count}} недель назад",
|
||||
"month_ago": "месяц назад",
|
||||
"months_ago": "{{count}} месяцев назад",
|
||||
"year_ago": "год назад",
|
||||
"years_ago": "{{count}} лет назад"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/tr/common.json
generated
16
webview-ui/src/i18n/locales/tr/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Mesajı Düzenle",
|
||||
"editWarning": "Bu mesajı düzenlemek, konuşmadaki sonraki tüm mesajları da silecektir. Devam etmek istiyor musun?",
|
||||
"proceed": "Devam Et"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "şimdi",
|
||||
"seconds_ago": "{{count}} saniye önce",
|
||||
"minute_ago": "bir dakika önce",
|
||||
"minutes_ago": "{{count}} dakika önce",
|
||||
"hour_ago": "bir saat önce",
|
||||
"hours_ago": "{{count}} saat önce",
|
||||
"day_ago": "bir gün önce",
|
||||
"days_ago": "{{count}} gün önce",
|
||||
"week_ago": "bir hafta önce",
|
||||
"weeks_ago": "{{count}} hafta önce",
|
||||
"month_ago": "bir ay önce",
|
||||
"months_ago": "{{count}} ay önce",
|
||||
"year_ago": "bir yıl önce",
|
||||
"years_ago": "{{count}} yıl önce"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/vi/common.json
generated
16
webview-ui/src/i18n/locales/vi/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "Chỉnh Sửa Tin Nhắn",
|
||||
"editWarning": "Chỉnh sửa tin nhắn này sẽ xóa tất cả các tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
|
||||
"proceed": "Tiếp Tục"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "vừa xong",
|
||||
"seconds_ago": "{{count}} giây trước",
|
||||
"minute_ago": "một phút trước",
|
||||
"minutes_ago": "{{count}} phút trước",
|
||||
"hour_ago": "một giờ trước",
|
||||
"hours_ago": "{{count}} giờ trước",
|
||||
"day_ago": "một ngày trước",
|
||||
"days_ago": "{{count}} ngày trước",
|
||||
"week_ago": "một tuần trước",
|
||||
"weeks_ago": "{{count}} tuần trước",
|
||||
"month_ago": "một tháng trước",
|
||||
"months_ago": "{{count}} tháng trước",
|
||||
"year_ago": "một năm trước",
|
||||
"years_ago": "{{count}} năm trước"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/zh-CN/common.json
generated
16
webview-ui/src/i18n/locales/zh-CN/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "编辑消息",
|
||||
"editWarning": "编辑此消息将删除对话中的所有后续消息。是否继续?",
|
||||
"proceed": "继续"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "刚刚",
|
||||
"seconds_ago": "{{count}}秒前",
|
||||
"minute_ago": "1分钟前",
|
||||
"minutes_ago": "{{count}}分钟前",
|
||||
"hour_ago": "1小时前",
|
||||
"hours_ago": "{{count}}小时前",
|
||||
"day_ago": "1天前",
|
||||
"days_ago": "{{count}}天前",
|
||||
"week_ago": "1周前",
|
||||
"weeks_ago": "{{count}}周前",
|
||||
"month_ago": "1个月前",
|
||||
"months_ago": "{{count}}个月前",
|
||||
"year_ago": "1年前",
|
||||
"years_ago": "{{count}}年前"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/zh-TW/common.json
generated
16
webview-ui/src/i18n/locales/zh-TW/common.json
generated
|
|
@ -67,5 +67,21 @@
|
|||
"editMessage": "編輯訊息",
|
||||
"editWarning": "編輯此訊息將刪除對話中的所有後續訊息。是否繼續?",
|
||||
"proceed": "繼續"
|
||||
},
|
||||
"time_ago": {
|
||||
"just_now": "剛剛",
|
||||
"seconds_ago": "{{count}}秒前",
|
||||
"minute_ago": "1分鐘前",
|
||||
"minutes_ago": "{{count}}分鐘前",
|
||||
"hour_ago": "1小時前",
|
||||
"hours_ago": "{{count}}小時前",
|
||||
"day_ago": "1天前",
|
||||
"days_ago": "{{count}}天前",
|
||||
"week_ago": "1週前",
|
||||
"weeks_ago": "{{count}}週前",
|
||||
"month_ago": "1個月前",
|
||||
"months_ago": "{{count}}個月前",
|
||||
"year_ago": "1年前",
|
||||
"years_ago": "{{count}}年前"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,154 @@
|
|||
// npx vitest src/utils/__tests__/format.spec.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { formatLargeNumber, formatDate, formatTimeAgo } from "../format"
|
||||
|
||||
import { formatDate } from "../format"
|
||||
// Mock i18next
|
||||
vi.mock("i18next", () => ({
|
||||
default: {
|
||||
t: vi.fn((key: string, options?: any) => {
|
||||
// Mock translations for testing
|
||||
const translations: Record<string, string> = {
|
||||
"common:number_format.billion_suffix": "b",
|
||||
"common:number_format.million_suffix": "m",
|
||||
"common:number_format.thousand_suffix": "k",
|
||||
"common:time_ago.just_now": "just now",
|
||||
"common:time_ago.seconds_ago": "{{count}} seconds ago",
|
||||
"common:time_ago.minute_ago": "a minute ago",
|
||||
"common:time_ago.minutes_ago": "{{count}} minutes ago",
|
||||
"common:time_ago.hour_ago": "an hour ago",
|
||||
"common:time_ago.hours_ago": "{{count}} hours ago",
|
||||
"common:time_ago.day_ago": "a day ago",
|
||||
"common:time_ago.days_ago": "{{count}} days ago",
|
||||
"common:time_ago.week_ago": "a week ago",
|
||||
"common:time_ago.weeks_ago": "{{count}} weeks ago",
|
||||
"common:time_ago.month_ago": "a month ago",
|
||||
"common:time_ago.months_ago": "{{count}} months ago",
|
||||
"common:time_ago.year_ago": "a year ago",
|
||||
"common:time_ago.years_ago": "{{count}} years ago",
|
||||
}
|
||||
|
||||
describe("formatDate", () => {
|
||||
it("formats a timestamp correctly", () => {
|
||||
// January 15, 2023, 10:30 AM
|
||||
const timestamp = new Date(2023, 0, 15, 10, 30).getTime()
|
||||
const result = formatDate(timestamp)
|
||||
let result = translations[key] || key
|
||||
if (options?.count !== undefined) {
|
||||
result = result.replace("{{count}}", options.count.toString())
|
||||
}
|
||||
return result
|
||||
}),
|
||||
language: "en",
|
||||
},
|
||||
}))
|
||||
|
||||
expect(result).toBe("JANUARY 15, 10:30 AM")
|
||||
describe("formatLargeNumber", () => {
|
||||
it("should format billions", () => {
|
||||
expect(formatLargeNumber(1500000000)).toBe("1.5b")
|
||||
expect(formatLargeNumber(2000000000)).toBe("2.0b")
|
||||
})
|
||||
|
||||
it("handles different months correctly", () => {
|
||||
// February 28, 2023, 3:45 PM
|
||||
const timestamp1 = new Date(2023, 1, 28, 15, 45).getTime()
|
||||
expect(formatDate(timestamp1)).toBe("FEBRUARY 28, 3:45 PM")
|
||||
|
||||
// December 31, 2023, 11:59 PM
|
||||
const timestamp2 = new Date(2023, 11, 31, 23, 59).getTime()
|
||||
expect(formatDate(timestamp2)).toBe("DECEMBER 31, 11:59 PM")
|
||||
it("should format millions", () => {
|
||||
expect(formatLargeNumber(1500000)).toBe("1.5m")
|
||||
expect(formatLargeNumber(2000000)).toBe("2.0m")
|
||||
})
|
||||
|
||||
it("handles AM/PM correctly", () => {
|
||||
// Morning time - 7:05 AM
|
||||
const morningTimestamp = new Date(2023, 5, 15, 7, 5).getTime()
|
||||
expect(formatDate(morningTimestamp)).toBe("JUNE 15, 7:05 AM")
|
||||
|
||||
// Noon - 12:00 PM
|
||||
const noonTimestamp = new Date(2023, 5, 15, 12, 0).getTime()
|
||||
expect(formatDate(noonTimestamp)).toBe("JUNE 15, 12:00 PM")
|
||||
|
||||
// Evening time - 8:15 PM
|
||||
const eveningTimestamp = new Date(2023, 5, 15, 20, 15).getTime()
|
||||
expect(formatDate(eveningTimestamp)).toBe("JUNE 15, 8:15 PM")
|
||||
it("should format thousands", () => {
|
||||
expect(formatLargeNumber(1500)).toBe("1.5k")
|
||||
expect(formatLargeNumber(2000)).toBe("2.0k")
|
||||
})
|
||||
|
||||
it("handles single-digit minutes with leading zeros", () => {
|
||||
// 9:05 AM
|
||||
const timestamp = new Date(2023, 3, 10, 9, 5).getTime()
|
||||
expect(formatDate(timestamp)).toBe("APRIL 10, 9:05 AM")
|
||||
})
|
||||
|
||||
it("converts the result to uppercase", () => {
|
||||
const timestamp = new Date(2023, 8, 21, 16, 45).getTime()
|
||||
const result = formatDate(timestamp)
|
||||
|
||||
expect(result).toBe(result.toUpperCase())
|
||||
expect(result).toBe("SEPTEMBER 21, 4:45 PM")
|
||||
it("should return string for small numbers", () => {
|
||||
expect(formatLargeNumber(999)).toBe("999")
|
||||
expect(formatLargeNumber(100)).toBe("100")
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatDate", () => {
|
||||
it("should format date in English", () => {
|
||||
const timestamp = new Date("2024-01-15T14:30:00").getTime()
|
||||
const result = formatDate(timestamp)
|
||||
// The exact format depends on the locale, but it should contain the date components
|
||||
expect(result).toMatch(/january|jan/i)
|
||||
expect(result).toMatch(/15/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatTimeAgo", () => {
|
||||
let originalDateNow: () => number
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Date.now to have a consistent "now" time
|
||||
originalDateNow = Date.now
|
||||
Date.now = vi.fn(() => new Date("2024-01-15T12:00:00").getTime())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original Date.now
|
||||
Date.now = originalDateNow
|
||||
})
|
||||
|
||||
it('should return "just now" for very recent times', () => {
|
||||
const timestamp = new Date("2024-01-15T11:59:35").getTime() // 25 seconds ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("just now")
|
||||
})
|
||||
|
||||
it("should format seconds ago", () => {
|
||||
const timestamp = new Date("2024-01-15T11:59:15").getTime() // 45 seconds ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("45 seconds ago")
|
||||
})
|
||||
|
||||
it("should format a minute ago", () => {
|
||||
const timestamp = new Date("2024-01-15T11:59:00").getTime() // 1 minute ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("a minute ago")
|
||||
})
|
||||
|
||||
it("should format minutes ago", () => {
|
||||
const timestamp = new Date("2024-01-15T11:45:00").getTime() // 15 minutes ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("15 minutes ago")
|
||||
})
|
||||
|
||||
it("should format an hour ago", () => {
|
||||
const timestamp = new Date("2024-01-15T11:00:00").getTime() // 1 hour ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("an hour ago")
|
||||
})
|
||||
|
||||
it("should format hours ago", () => {
|
||||
const timestamp = new Date("2024-01-15T09:00:00").getTime() // 3 hours ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("3 hours ago")
|
||||
})
|
||||
|
||||
it("should format a day ago", () => {
|
||||
const timestamp = new Date("2024-01-14T12:00:00").getTime() // 1 day ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("a day ago")
|
||||
})
|
||||
|
||||
it("should format days ago", () => {
|
||||
const timestamp = new Date("2024-01-12T12:00:00").getTime() // 3 days ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("3 days ago")
|
||||
})
|
||||
|
||||
it("should format a week ago", () => {
|
||||
const timestamp = new Date("2024-01-08T12:00:00").getTime() // 7 days ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("a week ago")
|
||||
})
|
||||
|
||||
it("should format weeks ago", () => {
|
||||
const timestamp = new Date("2024-01-01T12:00:00").getTime() // 14 days ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("2 weeks ago")
|
||||
})
|
||||
|
||||
it("should format a month ago", () => {
|
||||
const timestamp = new Date("2023-12-15T12:00:00").getTime() // ~1 month ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("a month ago")
|
||||
})
|
||||
|
||||
it("should format months ago", () => {
|
||||
const timestamp = new Date("2023-10-15T12:00:00").getTime() // ~3 months ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("3 months ago")
|
||||
})
|
||||
|
||||
it("should format a year ago", () => {
|
||||
const timestamp = new Date("2023-01-15T12:00:00").getTime() // 1 year ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("a year ago")
|
||||
})
|
||||
|
||||
it("should format years ago", () => {
|
||||
const timestamp = new Date("2021-01-15T12:00:00").getTime() // 3 years ago
|
||||
expect(formatTimeAgo(timestamp)).toBe("3 years ago")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,19 +17,59 @@ export const formatDate = (timestamp: number) => {
|
|||
const date = new Date(timestamp)
|
||||
const locale = i18next.language || "en"
|
||||
|
||||
// Get date format style from translations or use default transformations
|
||||
const dateStr = date.toLocaleString(locale, {
|
||||
return date.toLocaleString(locale, {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply transformations based on locale or use default
|
||||
if (locale === "en") {
|
||||
return dateStr.replace(", ", " ").replace(" at", ",").toUpperCase()
|
||||
export const formatTimeAgo = (timestamp: number) => {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
const weeks = Math.floor(days / 7)
|
||||
const months = Math.floor(days / 30)
|
||||
const years = Math.floor(days / 365)
|
||||
|
||||
if (years > 0) {
|
||||
return years === 1
|
||||
? i18next.t("common:time_ago.year_ago")
|
||||
: i18next.t("common:time_ago.years_ago", { count: years })
|
||||
}
|
||||
if (months > 0) {
|
||||
return months === 1
|
||||
? i18next.t("common:time_ago.month_ago")
|
||||
: i18next.t("common:time_ago.months_ago", { count: months })
|
||||
}
|
||||
if (weeks > 0) {
|
||||
return weeks === 1
|
||||
? i18next.t("common:time_ago.week_ago")
|
||||
: i18next.t("common:time_ago.weeks_ago", { count: weeks })
|
||||
}
|
||||
if (days > 0) {
|
||||
return days === 1
|
||||
? i18next.t("common:time_ago.day_ago")
|
||||
: i18next.t("common:time_ago.days_ago", { count: days })
|
||||
}
|
||||
if (hours > 0) {
|
||||
return hours === 1
|
||||
? i18next.t("common:time_ago.hour_ago")
|
||||
: i18next.t("common:time_ago.hours_ago", { count: hours })
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return minutes === 1
|
||||
? i18next.t("common:time_ago.minute_ago")
|
||||
: i18next.t("common:time_ago.minutes_ago", { count: minutes })
|
||||
}
|
||||
if (seconds > 30) {
|
||||
return i18next.t("common:time_ago.seconds_ago", { count: seconds })
|
||||
}
|
||||
|
||||
return dateStr.toUpperCase()
|
||||
return i18next.t("common:time_ago.just_now")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue