mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat(i18n): Implement internationalization for date distance
This commit is contained in:
@@ -23,11 +23,11 @@ import LongText from '@/components/long-text'
|
|||||||
import { AccessToken } from '../data/schema'
|
import { AccessToken } from '../data/schema'
|
||||||
import { DataTableColumnHeader } from './data-table-column-header'
|
import { DataTableColumnHeader } from './data-table-column-header'
|
||||||
import { DataTableRowActions } from './data-table-row-actions'
|
import { DataTableRowActions } from './data-table-row-actions'
|
||||||
import { format, formatDistanceToNow } from 'date-fns'
|
import { format, formatDistanceToNow, Locale } from 'date-fns'
|
||||||
import { AccountCellAction } from './account-action'
|
import { AccountCellAction } from './account-action'
|
||||||
import { AclCellAction } from './acl-action'
|
import { AclCellAction } from './acl-action'
|
||||||
|
|
||||||
export const getColumns = (t: (key: string) => string): ColumnDef<AccessToken>[] => [
|
export const getColumns = (t: (key: string) => string, locale: Locale): ColumnDef<AccessToken>[] => [
|
||||||
{
|
{
|
||||||
accessorKey: 'token',
|
accessorKey: 'token',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
@@ -114,7 +114,7 @@ export const getColumns = (t: (key: string) => string): ColumnDef<AccessToken>[]
|
|||||||
if (last_access_at === 0) {
|
if (last_access_at === 0) {
|
||||||
return <LongText className='max-w-40'>{t('accessTokens.notUsedYet')}</LongText>;
|
return <LongText className='max-w-40'>{t('accessTokens.notUsedYet')}</LongText>;
|
||||||
}
|
}
|
||||||
const result = formatDistanceToNow(new Date(last_access_at), { addSuffix: true });
|
const result = formatDistanceToNow(new Date(last_access_at), { addSuffix: true, locale });
|
||||||
return <LongText className='max-w-40'>{result}</LongText>;
|
return <LongText className='max-w-40'>{result}</LongText>;
|
||||||
},
|
},
|
||||||
meta: { className: 'w-40' },
|
meta: { className: 'w-40' },
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ import { list_access_tokens } from '@/api/access-tokens/api'
|
|||||||
import { TableSkeleton } from '@/components/table-skeleton'
|
import { TableSkeleton } from '@/components/table-skeleton'
|
||||||
import { FixedHeader } from '@/components/layout/fixed-header'
|
import { FixedHeader } from '@/components/layout/fixed-header'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { dateFnsLocaleMap } from '@/lib/utils'
|
||||||
|
import { enUS } from 'date-fns/locale'
|
||||||
|
|
||||||
export default function AccessTokens() {
|
export default function AccessTokens() {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS;
|
||||||
// Dialog states
|
// Dialog states
|
||||||
const [currentRow, setCurrentRow] = useState<AccessToken | null>(null)
|
const [currentRow, setCurrentRow] = useState<AccessToken | null>(null)
|
||||||
const [open, setOpen] = useDialogState<AccessTokensDialogType>(null)
|
const [open, setOpen] = useDialogState<AccessTokensDialogType>(null)
|
||||||
@@ -50,7 +53,7 @@ export default function AccessTokens() {
|
|||||||
queryFn: list_access_tokens,
|
queryFn: list_access_tokens,
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = getColumns(t)
|
const columns = getColumns(t, locale)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessTokensProvider value={{ open, setOpen, currentRow, setCurrentRow }}>
|
<AccessTokensProvider value={{ open, setOpen, currentRow, setCurrentRow }}>
|
||||||
@@ -58,43 +61,43 @@ export default function AccessTokens() {
|
|||||||
<FixedHeader />
|
<FixedHeader />
|
||||||
|
|
||||||
<Main>
|
<Main>
|
||||||
<div className="mx-auto mb-2 flex max-w-5xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-2">
|
<div className="mx-auto mb-2 flex max-w-5xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{t('accessTokens.title')}</h2>
|
<h2 className="text-2xl font-bold tracking-tight">{t('accessTokens.title')}</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('accessTokens.description')}
|
{t('accessTokens.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button className="space-x-1" onClick={() => setOpen('add')}>
|
<Button className="space-x-1" onClick={() => setOpen('add')}>
|
||||||
<span>{t('common.add')}</span> <Plus size={18} />
|
<span>{t('common.add')}</span> <Plus size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-auto flex-1 overflow-auto px-4 py-1 flex-row lg:space-x-12 space-y-0 max-w-5xl">
|
|
||||||
{isLoading ? (
|
|
||||||
<TableSkeleton columns={columns.length} rows={10} />
|
|
||||||
) : accessTokens?.length ? (
|
|
||||||
<AccessTokensTable data={accessTokens} columns={columns} />
|
|
||||||
) : (
|
|
||||||
<div className="flex h-[450px] shrink-0 items-center justify-center rounded-md border border-dashed">
|
|
||||||
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
|
|
||||||
<img
|
|
||||||
src={Logo}
|
|
||||||
className="max-h-[100px] w-auto opacity-20 saturate-0 transition-all duration-300 hover:opacity-100 hover:saturate-100 object-contain"
|
|
||||||
alt="Bichon Logo"
|
|
||||||
/>
|
|
||||||
<h3 className="mt-4 text-lg font-semibold">{t('accessTokens.noTokens')}</h3>
|
|
||||||
<p className="mb-4 mt-2 text-sm text-muted-foreground">
|
|
||||||
{t('accessTokens.noTokensDesc')}
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setOpen('add')}>{t('accessTokens.create')}</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
<div className="mx-auto flex-1 overflow-auto px-4 py-1 flex-row lg:space-x-12 space-y-0 max-w-5xl">
|
||||||
</div>
|
{isLoading ? (
|
||||||
</Main>
|
<TableSkeleton columns={columns.length} rows={10} />
|
||||||
|
) : accessTokens?.length ? (
|
||||||
|
<AccessTokensTable data={accessTokens} columns={columns} />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[450px] shrink-0 items-center justify-center rounded-md border border-dashed">
|
||||||
|
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
|
||||||
|
<img
|
||||||
|
src={Logo}
|
||||||
|
className="max-h-[100px] w-auto opacity-20 saturate-0 transition-all duration-300 hover:opacity-100 hover:saturate-100 object-contain"
|
||||||
|
alt="Bichon Logo"
|
||||||
|
/>
|
||||||
|
<h3 className="mt-4 text-lg font-semibold">{t('accessTokens.noTokens')}</h3>
|
||||||
|
<p className="mb-4 mt-2 text-sm text-muted-foreground">
|
||||||
|
{t('accessTokens.noTokensDesc')}
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setOpen('add')}>{t('accessTokens.create')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Main>
|
||||||
|
|
||||||
|
|
||||||
<TokensActionDialog
|
<TokensActionDialog
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import { IconCopy } from '@tabler/icons-react'
|
|||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { dateFnsLocaleMap } from '@/lib/utils'
|
||||||
|
import { enUS } from 'date-fns/locale'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentRow: AccountModel
|
currentRow: AccountModel
|
||||||
@@ -50,7 +52,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
|
export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS;
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { data: oauth2Tokens, isLoading } = useQuery({
|
const { data: oauth2Tokens, isLoading } = useQuery({
|
||||||
queryKey: ['oauth2-tokens', currentRow.id],
|
queryKey: ['oauth2-tokens', currentRow.id],
|
||||||
@@ -151,7 +154,7 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className='max-w-80'>{t('settings.updatedAt')}</TableCell>
|
<TableCell className='max-w-80'>{t('settings.updatedAt')}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDistanceToNow(new Date(oauth2Tokens.updated_at), { addSuffix: true })}
|
{formatDistanceToNow(new Date(oauth2Tokens.updated_at), { addSuffix: true, locale })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import { CheckCircle, Clock, Loader2, PlayCircle, FolderSync, FolderCheck } from 'lucide-react'
|
import { CheckCircle, Clock, Loader2, PlayCircle, FolderSync, FolderCheck } from 'lucide-react'
|
||||||
import { FolderSyncProgress } from './folder-sync-progress'
|
import { FolderSyncProgress } from './folder-sync-progress'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { dateFnsLocaleMap } from '@/lib/utils'
|
||||||
|
import { enUS } from 'date-fns/locale'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -42,7 +44,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS;
|
||||||
const { data: state, isLoading } = useQuery({
|
const { data: state, isLoading } = useQuery({
|
||||||
queryKey: ['running-state', currentRow.id],
|
queryKey: ['running-state', currentRow.id],
|
||||||
queryFn: () => account_state(currentRow.id),
|
queryFn: () => account_state(currentRow.id),
|
||||||
@@ -124,7 +127,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{state.initial_sync_start_time ? (
|
{state.initial_sync_start_time ? (
|
||||||
<span className="text-green-600">
|
<span className="text-green-600">
|
||||||
{formatDistanceToNow(new Date(state.initial_sync_start_time), { addSuffix: true })}
|
{formatDistanceToNow(new Date(state.initial_sync_start_time), { addSuffix: true, locale })}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-1 text-yellow-600">
|
<span className="flex items-center gap-1 text-yellow-600">
|
||||||
@@ -143,7 +146,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{state.initial_sync_end_time ? (
|
{state.initial_sync_end_time ? (
|
||||||
<span className="text-green-600">
|
<span className="text-green-600">
|
||||||
{formatDistanceToNow(new Date(state.initial_sync_end_time), { addSuffix: true })}
|
{formatDistanceToNow(new Date(state.initial_sync_end_time), { addSuffix: true, locale })}
|
||||||
</span>
|
</span>
|
||||||
) : state.initial_sync_start_time ? (
|
) : state.initial_sync_start_time ? (
|
||||||
<span className="flex items-center gap-1 text-blue-600">
|
<span className="flex items-center gap-1 text-blue-600">
|
||||||
@@ -192,7 +195,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<span className="text-muted-foreground">{t('accounts.runningState.startTime')}</span>
|
<span className="text-muted-foreground">{t('accounts.runningState.startTime')}</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{state.last_incremental_sync_start ? (
|
{state.last_incremental_sync_start ? (
|
||||||
formatDistanceToNow(new Date(state.last_incremental_sync_start), { addSuffix: true })
|
formatDistanceToNow(new Date(state.last_incremental_sync_start), { addSuffix: true, locale })
|
||||||
) : (
|
) : (
|
||||||
<span className="text-yellow-600">{t('accounts.runningState.notStarted')}</span>
|
<span className="text-yellow-600">{t('accounts.runningState.notStarted')}</span>
|
||||||
)}
|
)}
|
||||||
@@ -202,7 +205,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<span className="text-muted-foreground">{t('accounts.runningState.endTime')}</span>
|
<span className="text-muted-foreground">{t('accounts.runningState.endTime')}</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{state.last_incremental_sync_end ? (
|
{state.last_incremental_sync_end ? (
|
||||||
formatDistanceToNow(new Date(state.last_incremental_sync_end), { addSuffix: true })
|
formatDistanceToNow(new Date(state.last_incremental_sync_end), { addSuffix: true, locale })
|
||||||
) : state.last_incremental_sync_start ? (
|
) : state.last_incremental_sync_start ? (
|
||||||
<span className="text-blue-600">{t('accounts.runningState.inProgress')}</span>
|
<span className="text-blue-600">{t('accounts.runningState.inProgress')}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -243,7 +246,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-col gap-1">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
{formatDistanceToNow(new Date(item.at), { addSuffix: true })}
|
{formatDistanceToNow(new Date(item.at), { addSuffix: true, locale })}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium break-words">{item.error}</div>
|
<div className="font-medium break-words">{item.error}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import useMinimalAccountList from "@/hooks/use-minimal-account-list";
|
|||||||
import { VirtualizedSelect } from "@/components/virtualized-select";
|
import { VirtualizedSelect } from "@/components/virtualized-select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
interface AccountSwitcherProps {
|
interface AccountSwitcherProps {
|
||||||
@@ -35,7 +36,7 @@ export function AccountSwitcher({
|
|||||||
}: AccountSwitcherProps) {
|
}: AccountSwitcherProps) {
|
||||||
const { accountsOptions, isLoading } = useMinimalAccountList();
|
const { accountsOptions, isLoading } = useMinimalAccountList();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation();
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ export function AccountSwitcher({
|
|||||||
options={accountsOptions}
|
options={accountsOptions}
|
||||||
defaultValue={`${defaultAccountId}`}
|
defaultValue={`${defaultAccountId}`}
|
||||||
onSelectOption={(values) => onAccountSelect(parseInt(values[0], 10))}
|
onSelectOption={(values) => onAccountSelect(parseInt(values[0], 10))}
|
||||||
placeholder="Select an account"
|
placeholder={t('oauth2.selectAnAccount')}
|
||||||
noItemsComponent={<div className='space-y-2'>
|
noItemsComponent={<div className='space-y-2'>
|
||||||
<p>No active email account.</p>
|
<p>No active email account.</p>
|
||||||
<Button variant={'outline'} className="py-1 px-3 text-xs" onClick={() => navigate({ to: '/accounts' })}>Add Email Account</Button>
|
<Button variant={'outline'} className="py-1 px-3 text-xs" onClick={() => navigate({ to: '/accounts' })}>Add Email Account</Button>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { cn, formatBytes } from "@/lib/utils"
|
import { cn, dateFnsLocaleMap, formatBytes } from "@/lib/utils"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { MailIcon, Paperclip, Trash2 } from "lucide-react"
|
import { MailIcon, Paperclip, Trash2 } from "lucide-react"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
@@ -27,6 +27,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
|||||||
import { MailBulkActions } from "./bulk-actions"
|
import { MailBulkActions } from "./bulk-actions"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { enUS } from "date-fns/locale"
|
||||||
|
|
||||||
interface MailListProps {
|
interface MailListProps {
|
||||||
items: EmailEnvelope[]
|
items: EmailEnvelope[]
|
||||||
@@ -37,8 +38,9 @@ export function MailList({
|
|||||||
items,
|
items,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: MailListProps) {
|
}: MailListProps) {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { currentEnvelope, setCurrentEnvelope, setDeleteIds, setOpen, selected, setSelected } = useMailboxContext()
|
const { currentEnvelope, setCurrentEnvelope, setDeleteIds, setOpen, selected, setSelected } = useMailboxContext()
|
||||||
|
const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS;
|
||||||
|
|
||||||
const handleDelete = (envelope: EmailEnvelope) => {
|
const handleDelete = (envelope: EmailEnvelope) => {
|
||||||
setDeleteIds(new Set([envelope.id]))
|
setDeleteIds(new Set([envelope.id]))
|
||||||
@@ -177,7 +179,7 @@ export function MailList({
|
|||||||
<span className={cn(
|
<span className={cn(
|
||||||
isSelected ? "text-foreground font-medium" : "text-muted-foreground"
|
isSelected ? "text-foreground font-medium" : "text-muted-foreground"
|
||||||
)}>
|
)}>
|
||||||
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true })}
|
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true, locale })}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { cn, formatBytes } from "@/lib/utils"
|
import { cn, dateFnsLocaleMap, formatBytes } from "@/lib/utils"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react"
|
import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
@@ -29,6 +29,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { enUS } from "date-fns/locale"
|
||||||
|
|
||||||
interface MailListProps {
|
interface MailListProps {
|
||||||
items: EmailEnvelope[]
|
items: EmailEnvelope[]
|
||||||
@@ -41,7 +42,9 @@ export function MailList({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onEnvelopeChanged
|
onEnvelopeChanged
|
||||||
}: MailListProps) {
|
}: MailListProps) {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
|
const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS;
|
||||||
const { setOpen, currentEnvelope, setCurrentEnvelope, selected, setSelected, setToDelete } = useSearchContext()
|
const { setOpen, currentEnvelope, setCurrentEnvelope, selected, setSelected, setToDelete } = useSearchContext()
|
||||||
|
|
||||||
const handleToggleAll = () => {
|
const handleToggleAll = () => {
|
||||||
@@ -212,7 +215,7 @@ export function MailList({
|
|||||||
<span className="hidden md:inline">{formatBytes(item.size)}</span>
|
<span className="hidden md:inline">{formatBytes(item.size)}</span>
|
||||||
|
|
||||||
<span className={cn(isSelectedRow ? "text-foreground font-medium" : "text-muted-foreground")}>
|
<span className={cn(isSelectedRow ? "text-foreground font-medium" : "text-muted-foreground")}>
|
||||||
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true })}
|
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true, locale })}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
+48
-3
@@ -16,7 +16,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { enUS, zhCN, zhTW, arSA, de, es, fi, fr, it, ja, ko, nl, ptBR, ru, da, sv, nb, Locale } from 'date-fns/locale';
|
||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@@ -70,8 +70,12 @@ export function mapToRecordOfArrays(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumber(num: number) {
|
export function formatNumber(num: number): string {
|
||||||
return new Intl.NumberFormat('en-US').format(num);
|
const userLocale = navigator.language;
|
||||||
|
|
||||||
|
return new Intl.NumberFormat(userLocale, {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(num);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -127,3 +131,44 @@ export function formatTimestamp(milliseconds: number): string {
|
|||||||
const offsetMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, '0');
|
const offsetMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// i18n.language -> date-fns locale
|
||||||
|
export const dateFnsLocaleMap: Record<string, Locale> = {
|
||||||
|
en: enUS,
|
||||||
|
'en-us': enUS,
|
||||||
|
zh: zhCN,
|
||||||
|
'zh-cn': zhCN,
|
||||||
|
'zh-tw': zhTW,
|
||||||
|
'zh_hk': zhTW,
|
||||||
|
ar: arSA,
|
||||||
|
'ar-sa': arSA,
|
||||||
|
de: de,
|
||||||
|
'de-de': de,
|
||||||
|
es: es,
|
||||||
|
'es-es': es,
|
||||||
|
fi: fi,
|
||||||
|
'fi-fi': fi,
|
||||||
|
fr: fr,
|
||||||
|
'fr-fr': fr,
|
||||||
|
it: it,
|
||||||
|
'it-it': it,
|
||||||
|
jp: ja,
|
||||||
|
ja: ja,
|
||||||
|
'ja-jp': ja,
|
||||||
|
ko: ko,
|
||||||
|
'ko-kr': ko,
|
||||||
|
nl: nl,
|
||||||
|
'nl-nl': nl,
|
||||||
|
pt: ptBR,
|
||||||
|
'pt-br': ptBR,
|
||||||
|
ru: ru,
|
||||||
|
'ru-ru': ru,
|
||||||
|
da: da,
|
||||||
|
'da-dk': da,
|
||||||
|
sv: sv,
|
||||||
|
'sv-se': sv,
|
||||||
|
no: nb,
|
||||||
|
'no-no': nb,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user