diff --git a/components/files/__tests__/file-item-parts.test.tsx b/components/files/__tests__/file-item-parts.test.tsx new file mode 100644 index 000000000..09c76a75a --- /dev/null +++ b/components/files/__tests__/file-item-parts.test.tsx @@ -0,0 +1,27 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; + +let appLocale = 'ar'; +vi.mock('@/i18n/detect-locale', () => ({ getEffectiveLocale: () => appLocale })); + +import { FileSize, formatFullDate, formatShortDate } from '../file-item-parts'; + +// The browser here is en-US; the app's language must win. +describe('Files list dates and sizes', () => { + afterEach(() => { appLocale = 'ar'; }); + + it('writes dates in the app language, not the browser one', () => { + const now = new Date('2026-10-27T12:00:00Z'); + expect(formatShortDate('2026-03-05T10:00:00Z', now)).toMatch(/مارس/); + expect(formatFullDate('2026-03-05T10:00:00Z')).toMatch(/مارس/); + appLocale = 'de'; + expect(formatShortDate('2026-03-05T10:00:00Z', now)).toMatch(/März/); + }); + + it('keeps the number before the unit in a right-to-left layout', () => { + const { container } = render(
); + const size = container.querySelector('bdi')!; + expect(size).toHaveAttribute('dir', 'ltr'); + expect(size.textContent).toBe('6.7 KB'); + }); +}); diff --git a/components/files/file-details-panel.tsx b/components/files/file-details-panel.tsx index c8e98a0a5..5636c64f2 100644 --- a/components/files/file-details-panel.tsx +++ b/components/files/file-details-panel.tsx @@ -4,12 +4,12 @@ import { useMemo } from "react"; import { useTranslations } from "next-intl"; import { X, Download, Share2, Star, StarOff, FolderOpen, Eye, SquarePen, Files } from "@/components/icons"; import { Button } from "@/components/ui/button"; -import { cn, formatFileSize } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import type { FileResource } from "@/stores/file-store"; import type { FileResourceView } from "@/lib/plugin-types"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import { - ResourceIcon, Thumbnail, canShowThumbnail, formatFullDate, type IconOptions, + FileSize, ResourceIcon, Thumbnail, canShowThumbnail, formatFullDate, type IconOptions, } from "@/components/files/file-item-parts"; interface FileDetailsPanelProps { @@ -91,7 +91,7 @@ export function FileDetailsPanel({

{t("folder_summary", { folders, files })}

- + } /> {selection.length <= 1 && }
{selection.length <= 1 && ( @@ -160,7 +160,7 @@ export function FileDetailsPanel({ label={t("type")} value={resource.isDirectory || resource.isSymlink ? kindOf(resource, t) : (resource.contentType || kindOf(resource, t))} /> - {!resource.isDirectory && !resource.isSymlink && } + {!resource.isDirectory && !resource.isSymlink && } />} {resource.lastModified && } @@ -170,7 +170,7 @@ export function FileDetailsPanel({ ); } -function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) { +function Row({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) { return (
{label}
diff --git a/components/files/file-item-parts.tsx b/components/files/file-item-parts.tsx index 2b201fc3e..37a33b43e 100644 --- a/components/files/file-item-parts.tsx +++ b/components/files/file-item-parts.tsx @@ -3,8 +3,10 @@ import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { Share2, Users } from "@/components/icons"; -import { cn } from "@/lib/utils"; +import { cn, formatFileSize } from "@/lib/utils"; import { getEffectiveTimeZone } from "@/lib/timezone"; +import { getEffectiveLocale } from "@/i18n/detect-locale"; +import { useSettingsStore } from "@/stores/settings-store"; import type { FileResource } from "@/stores/file-store"; import { getFileIconByName, getFolderIcon, getSymlinkIcon, isImageFile, type FileIconSize } from "@/components/files/file-icons"; @@ -124,17 +126,33 @@ export function ShareBadge({ resource, t, className }: { return null; } +/** + * A size that keeps its number before its unit ("6.7 KB") in a right-to-left + * layout too, where the bidi algorithm would otherwise show "KB 6.7". + */ +export function FileSize({ bytes }: { bytes: number }) { + return {formatFileSize(bytes)}; +} + +/** Dates follow the app's language and 12/24-hour setting, as Mail's list does, not the browser's. */ +function dateLocale(): { locale: string; hour12: boolean } { + const locale = getEffectiveLocale(); + return { locale: locale === "en" ? "en-US" : locale, hour12: useSettingsStore.getState().timeFormat === "12h" }; +} + /** Full date and time, for tooltips and the details panel. */ export function formatFullDate(value: string): string { if (!value) return ""; try { - return new Date(value).toLocaleString(undefined, { + const { locale, hour12 } = dateLocale(); + return new Date(value).toLocaleString(locale, { timeZone: getEffectiveTimeZone(), year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", + hour12, }); } catch { return value; @@ -149,12 +167,13 @@ export function formatShortDate(value: string, now = new Date()): string { const timeZone = getEffectiveTimeZone(); const dayKey = (d: Date) => d.toLocaleDateString("en-CA", { timeZone }); try { + const { locale, hour12 } = dateLocale(); if (dayKey(date) === dayKey(now)) { - return date.toLocaleTimeString(undefined, { timeZone, hour: "2-digit", minute: "2-digit" }); + return date.toLocaleTimeString(locale, { timeZone, hour: "2-digit", minute: "2-digit", hour12 }); } const sameYear = date.toLocaleDateString("en-CA", { timeZone, year: "numeric" }) === now.toLocaleDateString("en-CA", { timeZone, year: "numeric" }); - return date.toLocaleDateString(undefined, { + return date.toLocaleDateString(locale, { timeZone, day: "numeric", month: "short", diff --git a/components/files/file-picker-dialog.tsx b/components/files/file-picker-dialog.tsx index 55a94e275..e49320ca0 100644 --- a/components/files/file-picker-dialog.tsx +++ b/components/files/file-picker-dialog.tsx @@ -175,7 +175,7 @@ export function FilePickerDialog({ client, onClose, onPick }: FilePickerDialogPr {node.name} {detail && {detail}} - {formatFileSize(node.size)} + {formatFileSize(node.size)} ); }; diff --git a/components/files/file-views.tsx b/components/files/file-views.tsx index f3d23336d..ce627400a 100644 --- a/components/files/file-views.tsx +++ b/components/files/file-views.tsx @@ -3,11 +3,11 @@ import { useRef } from "react"; import { useTranslations } from "next-intl"; import { ArrowUp, ArrowDown, MoreVertical, Download, Share2, Star, Check } from "@/components/icons"; -import { cn, formatFileSize } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import { IconButton } from "@/components/ui/icon-button"; import type { FileResource } from "@/stores/file-store"; import { - ResourceIcon, ShareBadge, Thumbnail, canShowThumbnail, formatFullDate, formatShortDate, + FileSize, ResourceIcon, ShareBadge, Thumbnail, canShowThumbnail, formatFullDate, formatShortDate, type IconOptions, } from "@/components/files/file-item-parts"; import type { SortDir, SortKey } from "@/components/files/files-header"; @@ -271,7 +271,7 @@ function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isS
{note && <>{note} · } {formatShortDate(resource.lastModified)} - {!resource.isDirectory && !resource.isSymlink && <> · {formatFileSize(resource.contentLength)}} + {!resource.isDirectory && !resource.isSymlink && <> · }
)}
@@ -280,7 +280,7 @@ function FileListRow({ resource, isSelected, isFocused, isDropTarget, isCut, isS {!touch && formatShortDate(resource.lastModified)}
- {!touch && (resource.isDirectory || resource.isSymlink ? "—" : formatFileSize(resource.contentLength))} + {!touch && (resource.isDirectory || resource.isSymlink ? "—" : )}
{!touch && !resource.isDirectory && !resource.isSymlink && ( @@ -482,7 +482,7 @@ function FileTile({ resource, isSelected, isFocused, isCut, selecting, touch, ic
- {resource.isSymlink ? "" : `${formatFileSize(resource.contentLength)} · `}{formatShortDate(resource.lastModified)} + {!resource.isSymlink && <> · }{formatShortDate(resource.lastModified)}