diff --git a/messages/en.json b/messages/en.json index 41572e1f0..08d72a873 100644 --- a/messages/en.json +++ b/messages/en.json @@ -628,6 +628,7 @@ "avgTokensPerMessage": "Avg Tokens/Message", "topModel": "Top Model", "summary": "Summary", + "last30Days": "last 30 days", "tokensAcross": "{tokens} tokens across {count} model{count, plural, =1 {} other {s}} in {period}.", "mostActive": "Most active: {model} ({tokens} tokens).", "summaryPrefix": "📊 Summary: ", diff --git a/messages/es.json b/messages/es.json index 7ed719b7b..34b3af8d8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -490,7 +490,8 @@ "failedToSaveImage": "Error al guardar imagen", "pleaseUploadValidImage": "Por favor sube una imagen válida (JPEG, PNG o WebP)", "imageSizeMustBeLessThan": "El tamaño de la imagen debe ser menor a 5MB", - "select": "Seleccionar" + "select": "Seleccionar", + "last30Days": "los últimos 30 días" } } } diff --git a/messages/fr.json b/messages/fr.json index 109546bf2..e39fa0339 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -490,7 +490,8 @@ "failedToSaveImage": "Échec de l'enregistrement de l'image", "pleaseUploadValidImage": "Veuillez télécharger une image valide (JPEG, PNG ou WebP)", "imageSizeMustBeLessThan": "La taille de l'image doit être inférieure à 5 Mo", - "select": "Sélectionner" + "select": "Sélectionner", + "last30Days": "les 30 derniers jours" } } } diff --git a/messages/ja.json b/messages/ja.json index d9bd20ba5..1e2da745e 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -489,7 +489,8 @@ "failedToSaveImage": "画像の保存に失敗しました", "pleaseUploadValidImage": "有効な画像をアップロードしてください(JPEG、PNG、またはWebP)", "imageSizeMustBeLessThan": "画像サイズは5MB未満である必要があります", - "select": "選択" + "select": "選択", + "last30Days": "過去30日間" } } } diff --git a/messages/ko.json b/messages/ko.json index 6b6553366..7bf40c7a6 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -504,7 +504,8 @@ "failedToSaveImage": "이미지 저장에 실패했습니다", "pleaseUploadValidImage": "유효한 이미지를 업로드하세요 (JPEG, PNG 또는 WebP)", "imageSizeMustBeLessThan": "이미지 크기는 5MB 미만이어야 합니다", - "select": "선택" + "select": "선택", + "last30Days": "최근 30일" } } } diff --git a/messages/no.json b/messages/no.json index 9dd648566..e593a93e8 100644 --- a/messages/no.json +++ b/messages/no.json @@ -628,6 +628,7 @@ "avgTokensPerMessage": "Gj.sn. tokens/melding", "topModel": "Toppmodell", "summary": "Sammendrag", + "last30Days": "de siste 30 dagene", "tokensAcross": "{tokens} tokens på tvers av {count} modell{count, plural, =1 {} other {er}} i {period}.", "mostActive": "Mest aktiv: {model} ({tokens} tokens).", "summaryPrefix": "📊 Sammendrag: ", diff --git a/messages/zh.json b/messages/zh.json index 670a5970b..9c5355161 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -489,7 +489,8 @@ "failedToSaveImage": "图像保存失败", "pleaseUploadValidImage": "请上传有效的图像(JPEG、PNG或WebP)", "imageSizeMustBeLessThan": "图像大小必须小于5MB", - "select": "选择" + "select": "选择", + "last30Days": "过去30天" } } } diff --git a/src/app/(chat)/archive/[id]/page.tsx b/src/app/(chat)/archive/[id]/page.tsx index 139688f5a..e3404ae2d 100644 --- a/src/app/(chat)/archive/[id]/page.tsx +++ b/src/app/(chat)/archive/[id]/page.tsx @@ -6,24 +6,11 @@ import { Card, CardContent, CardHeader } from "ui/card"; import { MessageCircleXIcon } from "lucide-react"; import { ArchiveActionsClient } from "@/app/(chat)/archive/[id]/archive-actions-client"; import { Separator } from "ui/separator"; +import { getFormatter, getTranslations } from "next-intl/server"; import LightRays from "ui/light-rays"; import Particles from "ui/particles"; -// Simple date formatting function -function formatTimeAgo(date: Date): string { - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); - - if (diffInDays === 0) return "Today"; - if (diffInDays === 1) return "Yesterday"; - if (diffInDays < 7) return `${diffInDays} days ago`; - if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} weeks ago`; - if (diffInDays < 365) return `${Math.floor(diffInDays / 30)} months ago`; - return `${Math.floor(diffInDays / 365)} years ago`; -} - interface ArchiveWithThreads { id: string; name: string; @@ -85,6 +72,9 @@ export default async function ArchivePage({ redirect("/"); } + const format = await getFormatter(); + const t = await getTranslations("User.Profile.common"); + return ( <> <> @@ -115,7 +105,7 @@ export default async function ArchivePage({

{archive.name}

- Created {formatTimeAgo(archive.createdAt)} + {t("created")} {format.relativeTime(archive.createdAt)}

@@ -166,7 +156,7 @@ export default async function ArchivePage({
- {formatTimeAgo( + {format.relativeTime( new Date(thread.lastMessageAt || thread.createdAt), )} diff --git a/src/components/admin/users-table.tsx b/src/components/admin/users-table.tsx index 693dde935..144f6bd07 100644 --- a/src/components/admin/users-table.tsx +++ b/src/components/admin/users-table.tsx @@ -2,7 +2,7 @@ import { useTransition, useCallback, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; -import { format } from "date-fns"; +import { useFormatter } from "next-intl"; import { Table, TableBody, @@ -62,6 +62,7 @@ export function UsersTable({ const formRef = useRef(null); const inputRef = useRef(null); const t = useTranslations("Admin.Users"); + const format = useFormatter(); const shouldAutoFocusRef = useRef(false); const submitForm = useCallback(() => { @@ -282,7 +283,7 @@ export function UsersTable({ /> - {format(new Date(user.createdAt), "MMM d, yyyy")} + {format.dateTime(new Date(user.createdAt), "short")} {t("Chat.ChatPreferences.exported")}{" "} - {formatDistanceToNow(new Date(exportItem.exportedAt), { - addSuffix: true, - })} + {format.relativeTime(new Date(exportItem.exportedAt))} {exportItem.expiresAt && ( <> {t("Chat.ChatPreferences.expires")}{" "} - {formatDistanceToNow( + {format.relativeTime( new Date(exportItem.expiresAt), - { - addSuffix: true, - }, )} diff --git a/src/components/export/chat-preview.tsx b/src/components/export/chat-preview.tsx index 8b46e26e9..ece108117 100644 --- a/src/components/export/chat-preview.tsx +++ b/src/components/export/chat-preview.tsx @@ -5,15 +5,16 @@ import { import { PreviewMessage } from "../message"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip"; -import { formatDate } from "date-fns"; +import { getFormatter } from "next-intl/server"; import Particles from "ui/particles"; import Comments from "./comments"; -export default function ChatPreview({ +export default async function ChatPreview({ thread, comments, }: { thread: ChatExportWithUser; comments: ChatExportCommentWithUser[] }) { + const format = await getFormatter(); return (
- {formatDate(thread.exportedAt, "MMM d, yyyy")} + {format.dateTime(new Date(thread.exportedAt), "short")}
diff --git a/src/components/export/comment.tsx b/src/components/export/comment.tsx index 5379e5ea0..4af6b14d7 100644 --- a/src/components/export/comment.tsx +++ b/src/components/export/comment.tsx @@ -3,7 +3,7 @@ import { ChatExportCommentWithUser } from "app-types/chat-export"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { Button } from "ui/button"; -import { formatDistanceToNow } from "date-fns"; +import { useFormatter } from "next-intl"; import { useState } from "react"; import { mutate } from "swr"; @@ -24,6 +24,7 @@ export default function Comment({ maxReplyDepth?: number; onReply?: () => void; }) { + const format = useFormatter(); const [isDeleting, setIsDeleting] = useState(false); const handleDelete = async () => { @@ -67,9 +68,7 @@ export default function Comment({
{comment.authorName} - {formatDistanceToNow(new Date(comment.createdAt), { - addSuffix: true, - })} + {format.relativeTime(new Date(comment.createdAt))}
diff --git a/src/components/shareable-card.tsx b/src/components/shareable-card.tsx index e0613928e..6dba0e40a 100644 --- a/src/components/shareable-card.tsx +++ b/src/components/shareable-card.tsx @@ -9,8 +9,7 @@ import { CardTitle, } from "ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; -import { useTranslations } from "next-intl"; -import { format } from "date-fns"; +import { useTranslations, useFormatter } from "next-intl"; import { cn } from "lib/utils"; import { ShareableActions, type Visibility } from "./shareable-actions"; import { WorkflowSummary } from "app-types/workflow"; @@ -53,6 +52,7 @@ export function ShareableCard({ actionsDisabled, }: ShareableCardProps) { const t = useTranslations(); + const format = useFormatter(); const isPublished = (item as WorkflowSummary).isPublished; const isBookmarked = type === "mcp" ? undefined : (item as AgentSummary).isBookmarked; @@ -92,7 +92,10 @@ export function ShareableCard({
{type === "workflow" && !isPublished && ( diff --git a/src/components/tool-invocation/interactive-table.tsx b/src/components/tool-invocation/interactive-table.tsx index 4ac5f9b24..998720717 100644 --- a/src/components/tool-invocation/interactive-table.tsx +++ b/src/components/tool-invocation/interactive-table.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useMemo } from "react"; +import { useFormatter } from "next-intl"; import { ArrowDownUp, Download, @@ -85,6 +86,7 @@ const loadXLSX = async () => { export function InteractiveTable(props: InteractiveTableProps) { const { title, data, columns, description } = props; + const format = useFormatter(); // Fixed settings for simplicity const pageSize = 20; @@ -107,12 +109,12 @@ export function InteractiveTable(props: InteractiveTableProps) { switch (columnType) { case "number": - return typeof value === "number" ? value.toLocaleString() : value; + return typeof value === "number" ? format.number(value) : value; case "boolean": return value ? "Yes" : "No"; case "date": try { - return new Date(value).toLocaleDateString(); + return format.dateTime(new Date(value), "short"); } catch { return value; } diff --git a/src/components/tool-invocation/web-search.tsx b/src/components/tool-invocation/web-search.tsx index e69a313af..421640664 100644 --- a/src/components/tool-invocation/web-search.tsx +++ b/src/components/tool-invocation/web-search.tsx @@ -6,7 +6,7 @@ import equal from "lib/equal"; import { notify } from "lib/notify"; import { cn, toAny } from "lib/utils"; import { AlertTriangleIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; +import { useTranslations, useFormatter } from "next-intl"; import { memo, useMemo, useState } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { GlobalIcon } from "ui/global-icon"; @@ -22,6 +22,7 @@ interface WebSearchToolInvocationProps { function PureWebSearchToolInvocation({ part }: WebSearchToolInvocationProps) { const t = useTranslations(); + const format = useFormatter(); const result = useMemo(() => { if (!part.state.startsWith("output")) return null; @@ -204,9 +205,10 @@ function PureWebSearchToolInvocation({ part }: WebSearchToolInvocationProps) { {result.publishedDate && (
Published:{" "} - {new Date( - result.publishedDate, - ).toLocaleDateString()} + {format.dateTime( + new Date(result.publishedDate), + "short", + )}
)}
diff --git a/src/components/user/user-detail/user-detail-form-card.tsx b/src/components/user/user-detail/user-detail-form-card.tsx index 26a9df638..2ea83c690 100644 --- a/src/components/user/user-detail/user-detail-form-card.tsx +++ b/src/components/user/user-detail/user-detail-form-card.tsx @@ -1,7 +1,7 @@ "use client"; import { useActionState, useState } from "react"; -import { format } from "date-fns"; +import { useFormatter } from "next-intl"; import { Card, CardContent, @@ -46,6 +46,7 @@ export function UserDetailFormCard({ view, }: UserDetailFormCardProps) { const { t, tCommon } = useProfileTranslations(view); + const format = useFormatter(); const [currentUser, setCurrentUser] = useState(user); const [, detailsUpdateFormAction, isPending] = useActionState< @@ -161,7 +162,7 @@ export function UserDetailFormCard({ {tCommon("joined")}

- {format(new Date(currentUser.createdAt), "PPP")} + {format.dateTime(new Date(currentUser.createdAt), "long")}

@@ -170,7 +171,7 @@ export function UserDetailFormCard({ {tCommon("lastUpdated")}

- {format(new Date(currentUser.updatedAt), "PPP")} + {format.dateTime(new Date(currentUser.updatedAt), "long")}

diff --git a/src/components/user/user-detail/user-info-card.tsx b/src/components/user/user-detail/user-info-card.tsx index 9be61ea2c..43ee06375 100644 --- a/src/components/user/user-detail/user-info-card.tsx +++ b/src/components/user/user-detail/user-info-card.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useActionState } from "react"; -import { format } from "date-fns"; +import { useFormatter } from "next-intl"; import { Card, CardContent } from "ui/card"; import { Avatar, AvatarImage, AvatarFallback } from "ui/avatar"; import { Label } from "ui/label"; @@ -38,6 +38,7 @@ export function UserInfoCard({ view, }: UserInfoCardProps) { const { t, tCommon } = useProfileTranslations(view); + const format = useFormatter(); const [editingField, setEditingField] = useState<"name" | "email" | null>( null, ); @@ -199,7 +200,7 @@ export function UserInfoCard({ {tCommon("joined")}

- {format(new Date(user.createdAt), "PPP")} + {format.dateTime(new Date(user.createdAt), "long")}

@@ -208,7 +209,7 @@ export function UserInfoCard({ {tCommon("lastUpdated")}

- {format(new Date(user.updatedAt), "PPP")} + {format.dateTime(new Date(user.updatedAt), "long")}

diff --git a/src/components/user/user-detail/user-sessions.tsx b/src/components/user/user-detail/user-sessions.tsx index f270c279e..d9e23b370 100644 --- a/src/components/user/user-detail/user-sessions.tsx +++ b/src/components/user/user-detail/user-sessions.tsx @@ -14,8 +14,7 @@ import { TableHeader, TableRow, } from "ui/table"; -import { format } from "date-fns"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, getFormatter } from "next-intl/server"; interface UserSessionsProps { userId: string; @@ -30,6 +29,7 @@ export async function UserSessions({ view === "admin" ? "User.Profile.admin" : "User.Profile.user", ); const tCommon = await getTranslations("User.Profile.common"); + const format = await getFormatter(); const sessions = await getUserSessions(userId); return ( @@ -57,10 +57,16 @@ export async function UserSessions({ {sessions.map((session) => ( - {format(new Date(session.createdAt), "PPp")} + {format.dateTime( + new Date(session.createdAt), + "shortWithTime", + )} - {format(new Date(session.expiresAt), "PPp")} + {format.dateTime( + new Date(session.expiresAt), + "shortWithTime", + )} {session.ipAddress || tCommon("unknown")} diff --git a/src/components/user/user-detail/user-statistics-card.tsx b/src/components/user/user-detail/user-statistics-card.tsx index 1f751bd7a..db16911f1 100644 --- a/src/components/user/user-detail/user-statistics-card.tsx +++ b/src/components/user/user-detail/user-statistics-card.tsx @@ -43,7 +43,7 @@ export function UserStatisticsCard({ stats, view }: UserStatisticsCardProps) { {tCommon("usageStatistics")}

- {t("aiModelUsageFor", { period: stats.period })} + {t("aiModelUsageFor", { period: tCommon(stats.period) })}

@@ -253,7 +253,7 @@ export function UserStatisticsCard({ stats, view }: UserStatisticsCardProps) { {tCommon("tokensAcross", { tokens: stats.totalTokens.toLocaleString(), count: stats.modelStats.length, - period: stats.period.toLowerCase(), + period: tCommon(stats.period), })} {stats.modelStats[0] && ( <> diff --git a/src/components/user/user-detail/user-stats-card-loader.tsx b/src/components/user/user-detail/user-stats-card-loader.tsx index ae891748a..7d539e220 100644 --- a/src/components/user/user-detail/user-stats-card-loader.tsx +++ b/src/components/user/user-detail/user-stats-card-loader.tsx @@ -11,12 +11,7 @@ export const UserStatsCardLoader = async ({ view?: "admin" | "user"; }) => { const userStats = await getUserStats(userId); - return ( - - ); + return ; }; export const UserStatsCardLoaderSkeleton = () => { diff --git a/src/components/workflow/node-result-popup.tsx b/src/components/workflow/node-result-popup.tsx index 8bf4d0c6b..48de329ff 100644 --- a/src/components/workflow/node-result-popup.tsx +++ b/src/components/workflow/node-result-popup.tsx @@ -24,7 +24,7 @@ import { useCopy } from "@/hooks/use-copy"; import { Button } from "ui/button"; import { cn, errorToString } from "lib/utils"; import { Alert, AlertDescription, AlertTitle } from "ui/alert"; -import { useTranslations } from "next-intl"; +import { useTranslations, useFormatter } from "next-intl"; export function NodeResultPopup({ history, @@ -40,6 +40,7 @@ export function NodeResultPopup({ }) { const { copy, copied } = useCopy(); const t = useTranslations(); + const format = useFormatter(); const [tab, setTab] = useState<"input" | "output">("output"); @@ -90,7 +91,9 @@ export function NodeResultPopup({

{t("Common.startedAt")}

-

{new Date(history.startedAt).toLocaleString()}

+

+ {format.dateTime(new Date(history.startedAt), "shortWithTime")} +

diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 5d1007140..901917657 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -20,6 +20,30 @@ export default getRequestConfig(async () => { locale, messages: locale === "en" ? defaultMessages : deepmerge(defaultMessages, messages), + formats: { + dateTime: { + // "Nov 20, 2020" + short: { + day: "numeric", + month: "short", + year: "numeric", + }, + // "November 20, 2020" + long: { + day: "numeric", + month: "long", + year: "numeric", + }, + // "Nov 20, 2020, 10:36 AM" + shortWithTime: { + day: "numeric", + month: "short", + year: "numeric", + hour: "numeric", + minute: "numeric", + }, + }, + }, getMessageFallback({ key, namespace }) { return `${namespace}.${key}`; }, diff --git a/src/lib/db/pg/repositories/user-repository.pg.ts b/src/lib/db/pg/repositories/user-repository.pg.ts index 971527d3b..7502a417d 100644 --- a/src/lib/db/pg/repositories/user-repository.pg.ts +++ b/src/lib/db/pg/repositories/user-repository.pg.ts @@ -159,7 +159,7 @@ export const pgUserRepository: UserRepository = { totalTokens: Number(stat.totalTokens || 0), })), totalTokens, - period: "Last 30 Days", + period: "last30Days", }; }, getUserAuthMethods: async (userId: string) => {