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) => {