mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 10:05:03 +08:00
date localization
This commit is contained in:
@@ -424,6 +424,7 @@
|
|||||||
"Names do not match": "Names do not match",
|
"Names do not match": "Names do not match",
|
||||||
"Today, {{time}}": "Today, {{time}}",
|
"Today, {{time}}": "Today, {{time}}",
|
||||||
"Yesterday, {{time}}": "Yesterday, {{time}}",
|
"Yesterday, {{time}}": "Yesterday, {{time}}",
|
||||||
|
"now": "now",
|
||||||
"Space created successfully": "Space created successfully",
|
"Space created successfully": "Space created successfully",
|
||||||
"Space updated successfully": "Space updated successfully",
|
"Space updated successfully": "Space updated successfully",
|
||||||
"Space deleted successfully": "Space deleted successfully",
|
"Space deleted successfully": "Space deleted successfully",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
||||||
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IApiKey } from "@/ee/api-key";
|
import { IApiKey } from "@/ee/api-key";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import NoTableResults from "@/components/common/no-table-results";
|
import NoTableResults from "@/components/common/no-table-results";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
interface ApiKeyTableProps {
|
interface ApiKeyTableProps {
|
||||||
apiKeys: IApiKey[];
|
apiKeys: IApiKey[];
|
||||||
@@ -23,10 +23,11 @@ export function ApiKeyTable({
|
|||||||
onRevoke,
|
onRevoke,
|
||||||
}: ApiKeyTableProps) {
|
}: ApiKeyTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
const formatDate = (date: Date | string | null) => {
|
const formatDate = (date: Date | string | null) => {
|
||||||
if (!date) return t("Never");
|
if (!date) return t("Never");
|
||||||
return format(new Date(date), "MMM dd, yyyy");
|
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isExpired = (expiresAt: string | null) => {
|
const isExpired = (expiresAt: string | null) => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function CreateApiKeyModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: CreateApiKeyModalProps) {
|
}: CreateApiKeyModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const [expirationOption, setExpirationOption] = useState<string>("30");
|
const [expirationOption, setExpirationOption] = useState<string>("30");
|
||||||
const createApiKeyMutation = useCreateApiKeyMutation();
|
const createApiKeyMutation = useCreateApiKeyMutation();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export function CreateApiKeyModal({
|
|||||||
const getExpirationLabel = (days: number) => {
|
const getExpirationLabel = (days: number) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setDate(date.getDate() + days);
|
date.setDate(date.getDate() + days);
|
||||||
const formatted = date.toLocaleDateString("en-US", {
|
const formatted = date.toLocaleDateString(i18n.language, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import {
|
|||||||
} from "@/ee/billing/queries/billing-query.ts";
|
} from "@/ee/billing/queries/billing-query.ts";
|
||||||
import { Group, Text, SimpleGrid, Paper } from "@mantine/core";
|
import { Group, Text, SimpleGrid, Paper } from "@mantine/core";
|
||||||
import classes from "./billing.module.css";
|
import classes from "./billing.module.css";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { formatInterval } from "@/ee/billing/utils.ts";
|
import { formatInterval } from "@/ee/billing/utils.ts";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
export default function BillingDetails() {
|
export default function BillingDetails() {
|
||||||
const { data: billing } = useBillingQuery();
|
const { data: billing } = useBillingQuery();
|
||||||
const { data: plans } = useBillingPlans();
|
const { data: plans } = useBillingPlans();
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
if (!billing || !plans) {
|
if (!billing || !plans) {
|
||||||
return null;
|
return null;
|
||||||
@@ -75,7 +76,12 @@ export default function BillingDetails() {
|
|||||||
: "Renewal date"}
|
: "Renewal date"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fw={700} fz="lg">
|
<Text fw={700} fz="lg">
|
||||||
{format(billing.periodEndAt, "dd MMM, yyyy")}
|
{formatLocalized(
|
||||||
|
billing.periodEndAt,
|
||||||
|
"dd MMM, yyyy",
|
||||||
|
"PP",
|
||||||
|
locale,
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Badge, Table } from "@mantine/core";
|
import { Badge, Table } from "@mantine/core";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useLicenseInfo } from "@/ee/licence/queries/license-query.ts";
|
import { useLicenseInfo } from "@/ee/licence/queries/license-query.ts";
|
||||||
import { isLicenseExpired } from "@/ee/licence/license.utils.ts";
|
import { isLicenseExpired } from "@/ee/licence/license.utils.ts";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
export default function LicenseDetails() {
|
export default function LicenseDetails() {
|
||||||
const { data: license, isError } = useLicenseInfo();
|
const { data: license, isError } = useLicenseInfo();
|
||||||
const [workspace] = useAtom(workspaceAtom);
|
const [workspace] = useAtom(workspaceAtom);
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
if (!license) {
|
if (!license) {
|
||||||
return null;
|
return null;
|
||||||
@@ -50,12 +51,16 @@ export default function LicenseDetails() {
|
|||||||
|
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>Issued at</Table.Th>
|
<Table.Th>Issued at</Table.Th>
|
||||||
<Table.Td>{format(license.issuedAt, "dd MMMM, yyyy")}</Table.Td>
|
<Table.Td>
|
||||||
|
{formatLocalized(license.issuedAt, "dd MMMM, yyyy", "PPP", locale)}
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>Expires at</Table.Th>
|
<Table.Th>Expires at</Table.Th>
|
||||||
<Table.Td>{format(license.expiresAt, "dd MMMM, yyyy")}</Table.Td>
|
<Table.Td>
|
||||||
|
{formatLocalized(license.expiresAt, "dd MMMM, yyyy", "PPP", locale)}
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>License ID</Table.Th>
|
<Table.Th>License ID</Table.Th>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Group, NumberInput, Select, Text } from "@mantine/core";
|
import { Group, NumberInput, Select, Text } from "@mantine/core";
|
||||||
import { DateInput } from "@mantine/dates";
|
import { DateInput } from "@mantine/dates";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n.ts";
|
||||||
import {
|
import {
|
||||||
ExpirationMode,
|
ExpirationMode,
|
||||||
PeriodUnit,
|
PeriodUnit,
|
||||||
@@ -30,7 +31,7 @@ export function addDays(days: number, from?: Date): Date {
|
|||||||
|
|
||||||
function formatShortDate(date: Date): string {
|
function formatShortDate(date: Date): string {
|
||||||
const crossesYear = date.getFullYear() !== new Date().getFullYear();
|
const crossesYear = date.getFullYear() !== new Date().getFullYear();
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(i18n.language, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
...(crossesYear && { year: "numeric" }),
|
...(crossesYear && { year: "numeric" }),
|
||||||
@@ -38,7 +39,7 @@ function formatShortDate(date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatLongDate(date: Date): string {
|
function formatLongDate(date: Date): string {
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(i18n.language, {
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n.ts";
|
||||||
import {
|
import {
|
||||||
useMarkObsoleteMutation,
|
useMarkObsoleteMutation,
|
||||||
usePageVerificationInfoQuery,
|
usePageVerificationInfoQuery,
|
||||||
@@ -197,11 +198,14 @@ function ExpiringManageContent({ pageId, info, onClose }: ManageContentProps) {
|
|||||||
{info.expiresAt && (
|
{info.expiresAt && (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{t(status === "expired" ? "Expired {{date}}" : "Expires {{date}}", {
|
{t(status === "expired" ? "Expired {{date}}" : "Expires {{date}}", {
|
||||||
date: new Date(info.expiresAt).toLocaleDateString(undefined, {
|
date: new Date(info.expiresAt).toLocaleDateString(
|
||||||
month: "long",
|
i18n.language,
|
||||||
day: "numeric",
|
{
|
||||||
year: "numeric",
|
month: "long",
|
||||||
}),
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
),
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
IconShieldCheck,
|
IconShieldCheck,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n.ts";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { usePageQuery } from "@/features/page/queries/page-query";
|
import { usePageQuery } from "@/features/page/queries/page-query";
|
||||||
@@ -127,7 +128,7 @@ export function PageVerificationBadge({
|
|||||||
status === "verified" && verificationInfo?.expiresAt
|
status === "verified" && verificationInfo?.expiresAt
|
||||||
? t("Verified until {{date}}", {
|
? t("Verified until {{date}}", {
|
||||||
date: new Date(verificationInfo.expiresAt).toLocaleDateString(
|
date: new Date(verificationInfo.expiresAt).toLocaleDateString(
|
||||||
undefined,
|
i18n.language,
|
||||||
{ month: "long", day: "numeric", year: "numeric" },
|
{ month: "long", day: "numeric", year: "numeric" },
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import {
|
|||||||
} from "@/ee/page-verification/types/page-verification.types";
|
} from "@/ee/page-verification/types/page-verification.types";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils";
|
import { buildPageUrl } from "@/features/page/page.utils";
|
||||||
import { format } from "date-fns";
|
|
||||||
import NoTableResults from "@/components/common/no-table-results";
|
import NoTableResults from "@/components/common/no-table-results";
|
||||||
import rowClasses from "@/components/ui/clickable-table-row.module.css";
|
import rowClasses from "@/components/ui/clickable-table-row.module.css";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
import type { Locale } from "date-fns";
|
||||||
|
|
||||||
const MAX_VISIBLE_VERIFIERS = 5;
|
const MAX_VISIBLE_VERIFIERS = 5;
|
||||||
|
|
||||||
@@ -48,7 +49,11 @@ function statusBadge(status: VerificationStatus | null, t: (s: string) => string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifiedUntilText(item: IVerificationListItem, t: (s: string) => string): string {
|
function verifiedUntilText(
|
||||||
|
item: IVerificationListItem,
|
||||||
|
t: (s: string) => string,
|
||||||
|
locale: Locale,
|
||||||
|
): string {
|
||||||
if (item.type === "qms") {
|
if (item.type === "qms") {
|
||||||
if (item.status === "approved") return t("Indefinitely");
|
if (item.status === "approved") return t("Indefinitely");
|
||||||
return "—";
|
return "—";
|
||||||
@@ -60,7 +65,7 @@ function verifiedUntilText(item: IVerificationListItem, t: (s: string) => string
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
if (expires <= now) return t("Expired");
|
if (expires <= now) return t("Expired");
|
||||||
return format(expires, "MMM d, yyyy");
|
return formatLocalized(expires, "MMM d, yyyy", "PP", locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableSkeleton() {
|
function TableSkeleton() {
|
||||||
@@ -98,6 +103,7 @@ export default function VerificationListTable({
|
|||||||
isLoading,
|
isLoading,
|
||||||
}: VerificationListTableProps) {
|
}: VerificationListTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.ScrollContainer minWidth={600}>
|
<Table.ScrollContainer minWidth={600}>
|
||||||
@@ -200,7 +206,7 @@ export default function VerificationListTable({
|
|||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
{verifiedUntilText(item, t)}
|
{verifiedUntilText(item, t, locale)}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
||||||
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import NoTableResults from "@/components/common/no-table-results";
|
import NoTableResults from "@/components/common/no-table-results";
|
||||||
import { IScimToken } from "@/ee/scim/types/scim-token.types";
|
import { IScimToken } from "@/ee/scim/types/scim-token.types";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
interface ScimTokenTableProps {
|
interface ScimTokenTableProps {
|
||||||
tokens: IScimToken[];
|
tokens: IScimToken[];
|
||||||
@@ -21,10 +21,11 @@ export function ScimTokenTable({
|
|||||||
onRevoke,
|
onRevoke,
|
||||||
}: ScimTokenTableProps) {
|
}: ScimTokenTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
const formatDate = (date: Date | string | null) => {
|
const formatDate = (date: Date | string | null) => {
|
||||||
if (!date) return t("Never");
|
if (!date) return t("Never");
|
||||||
return format(new Date(date), "MMM dd, yyyy");
|
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-2
@@ -36,13 +36,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
const setEmbed = (provider: string) =>
|
const setEmbed = (provider: string) =>
|
||||||
editor.chain().focus().setEmbed({ provider }).run();
|
editor.chain().focus().setEmbed({ provider }).run();
|
||||||
|
|
||||||
const insertDate = () => {
|
const insertDate = () => {
|
||||||
const currentDate = new Date().toLocaleDateString("en-US", {
|
const currentDate = new Date().toLocaleDateString(i18n.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import IconMermaid from "@/components/icons/icon-mermaid";
|
|||||||
import IconDrawio from "@/components/icons/icon-drawio";
|
import IconDrawio from "@/components/icons/icon-drawio";
|
||||||
import { IconColumns4 } from "@/components/icons/icon-columns-4";
|
import { IconColumns4 } from "@/components/icons/icon-columns-4";
|
||||||
import { IconColumns5 } from "@/components/icons/icon-columns-5";
|
import { IconColumns5 } from "@/components/icons/icon-columns-5";
|
||||||
|
import i18n from "@/i18n.ts";
|
||||||
import {
|
import {
|
||||||
AirtableIcon,
|
AirtableIcon,
|
||||||
FigmaIcon,
|
FigmaIcon,
|
||||||
@@ -459,7 +460,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
searchTerms: ["date", "today"],
|
searchTerms: ["date", "today"],
|
||||||
icon: IconCalendar,
|
icon: IconCalendar,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
const currentDate = new Date().toLocaleDateString("en-US", {
|
const currentDate = new Date().toLocaleDateString(i18n.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
import { format, isThisYear, isToday, isYesterday } from "date-fns";
|
import { isThisYear, isToday, isYesterday } from "date-fns";
|
||||||
import i18n from "@/i18n.ts";
|
import i18n from "@/i18n.ts";
|
||||||
|
import { formatLocalized, getDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
export function formatLabelListDate(date: Date): string {
|
export function formatLabelListDate(date: Date): string {
|
||||||
|
const locale = getDateFnsLocale();
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
return i18n.t("Today, {{time}}", { time: format(date, "h:mma") });
|
return i18n.t("Today, {{time}}", {
|
||||||
|
time: formatLocalized(date, "h:mma", "p", locale),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (isYesterday(date)) {
|
if (isYesterday(date)) {
|
||||||
return i18n.t("Yesterday, {{time}}", { time: format(date, "h:mma") });
|
return i18n.t("Yesterday, {{time}}", {
|
||||||
|
time: formatLocalized(date, "h:mma", "p", locale),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (isThisYear(date)) {
|
if (isThisYear(date)) {
|
||||||
return format(date, "MMM dd");
|
if (locale.code?.startsWith("en")) {
|
||||||
|
return formatLocalized(date, "MMM dd", "MMM dd", locale);
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat(i18n.language, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date);
|
||||||
}
|
}
|
||||||
return format(date, "MMM dd, yyyy");
|
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import i18n from "@/i18n.ts";
|
||||||
import { INotification } from "./types/notification.types";
|
import { INotification } from "./types/notification.types";
|
||||||
|
|
||||||
export function formatRelativeTime(dateStr: string): string {
|
export function formatRelativeTime(dateStr: string): string {
|
||||||
@@ -8,15 +9,15 @@ export function formatRelativeTime(dateStr: string): string {
|
|||||||
const diffHours = Math.floor(diffMs / 3_600_000);
|
const diffHours = Math.floor(diffMs / 3_600_000);
|
||||||
const diffDays = Math.floor(diffMs / 86_400_000);
|
const diffDays = Math.floor(diffMs / 86_400_000);
|
||||||
|
|
||||||
if (diffMin < 1) return "now";
|
if (diffMin < 1) return i18n.t("now");
|
||||||
if (diffMin < 60) return `${diffMin}m`;
|
if (diffMin < 60) return `${diffMin}m`;
|
||||||
if (diffHours < 24) return `${diffHours}h`;
|
if (diffHours < 24) return `${diffHours}h`;
|
||||||
if (diffDays < 7) return `${diffDays}d`;
|
if (diffDays < 7) return `${diffDays}d`;
|
||||||
|
|
||||||
return date.toLocaleDateString(undefined, {
|
return new Intl.DateTimeFormat(i18n.language, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeGroup = "today" | "yesterday" | "this_week" | "older";
|
type TimeGroup = "today" | "yesterday" | "this_week" | "older";
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import Paginate from "@/components/common/paginate.tsx";
|
|||||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||||
import { useGetSharesQuery } from "@/features/share/queries/share-query.ts";
|
import { useGetSharesQuery } from "@/features/share/queries/share-query.ts";
|
||||||
import { ISharedItem } from "@/features/share/types/share.types.ts";
|
import { ISharedItem } from "@/features/share/types/share.types.ts";
|
||||||
import { format } from "date-fns";
|
|
||||||
import ShareActionMenu from "@/features/share/components/share-action-menu.tsx";
|
import ShareActionMenu from "@/features/share/components/share-action-menu.tsx";
|
||||||
|
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import { getPageIcon } from "@/lib";
|
import { getPageIcon } from "@/lib";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
@@ -20,6 +20,7 @@ export default function ShareList() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||||
const { data, isLoading } = useGetSharesQuery({ cursor });
|
const { data, isLoading } = useGetSharesQuery({ cursor });
|
||||||
|
const locale = useDateFnsLocale();
|
||||||
|
|
||||||
if (!isLoading && data?.items.length === 0) {
|
if (!isLoading && data?.items.length === 0) {
|
||||||
return <EmptyState icon={IconWorld} title={t("No shared pages")} />;
|
return <EmptyState icon={IconWorld} title={t("No shared pages")} />;
|
||||||
@@ -81,7 +82,12 @@ export default function ShareList() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
{format(new Date(share.createdAt), "MMM dd, yyyy")}
|
{formatLocalized(
|
||||||
|
share.createdAt,
|
||||||
|
"MMM dd, yyyy",
|
||||||
|
"PP",
|
||||||
|
locale,
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { format as dateFnsFormat, type Locale } from "date-fns";
|
||||||
|
import {
|
||||||
|
de,
|
||||||
|
enUS,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
it,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
nl,
|
||||||
|
ptBR,
|
||||||
|
ru,
|
||||||
|
uk,
|
||||||
|
zhCN,
|
||||||
|
} from "date-fns/locale";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n.ts";
|
||||||
|
|
||||||
|
const LOCALE_MAP: Record<string, Locale> = {
|
||||||
|
"de-DE": de,
|
||||||
|
"en-US": enUS,
|
||||||
|
"es-ES": es,
|
||||||
|
"fr-FR": fr,
|
||||||
|
"it-IT": it,
|
||||||
|
"ja-JP": ja,
|
||||||
|
"ko-KR": ko,
|
||||||
|
"nl-NL": nl,
|
||||||
|
"pt-BR": ptBR,
|
||||||
|
"ru-RU": ru,
|
||||||
|
"uk-UA": uk,
|
||||||
|
"zh-CN": zhCN,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDateFnsLocale(language?: string): Locale {
|
||||||
|
const lang = language ?? i18n.language ?? "en-US";
|
||||||
|
return LOCALE_MAP[lang] ?? LOCALE_MAP[lang.split("-")[0]] ?? enUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDateFnsLocale(): Locale {
|
||||||
|
const { i18n: instance } = useTranslation();
|
||||||
|
return getDateFnsLocale(instance.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnglishLocale(locale: Locale): boolean {
|
||||||
|
return locale.code === "en-US" || locale.code?.startsWith("en") === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks `enUSPattern` for the English locale and `localizedPattern` for every
|
||||||
|
* other locale. Keeps existing en-US output byte-identical while letting other
|
||||||
|
* languages use date-fns localized format tokens (P, PP, p, PPp, …).
|
||||||
|
*/
|
||||||
|
export function formatLocalized(
|
||||||
|
date: Date | number | string,
|
||||||
|
enUSPattern: string,
|
||||||
|
localizedPattern: string,
|
||||||
|
locale?: Locale,
|
||||||
|
): string {
|
||||||
|
const effective = locale ?? getDateFnsLocale();
|
||||||
|
const pattern = isEnglishLocale(effective) ? enUSPattern : localizedPattern;
|
||||||
|
return dateFnsFormat(new Date(date), pattern, { locale: effective });
|
||||||
|
}
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
import { formatDistanceStrict } from "date-fns";
|
import { formatDistanceStrict, isToday, isYesterday } from "date-fns";
|
||||||
import { format, isToday, isYesterday } from "date-fns";
|
|
||||||
import i18n from "@/i18n.ts";
|
import i18n from "@/i18n.ts";
|
||||||
|
import { formatLocalized, getDateFnsLocale } from "@/lib/date-locale.ts";
|
||||||
|
|
||||||
export function timeAgo(date: Date) {
|
export function timeAgo(date: Date) {
|
||||||
return formatDistanceStrict(new Date(date), new Date(), { addSuffix: true });
|
return formatDistanceStrict(new Date(date), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
locale: getDateFnsLocale(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formattedDate(date: Date) {
|
export function formattedDate(date: Date) {
|
||||||
|
const locale = getDateFnsLocale();
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
return i18n.t("Today, {{time}}", { time: format(date, "h:mma") });
|
return i18n.t("Today, {{time}}", {
|
||||||
|
time: formatLocalized(date, "h:mma", "p", locale),
|
||||||
|
});
|
||||||
} else if (isYesterday(date)) {
|
} else if (isYesterday(date)) {
|
||||||
return i18n.t("Yesterday, {{time}}", { time: format(date, "h:mma") });
|
return i18n.t("Yesterday, {{time}}", {
|
||||||
|
time: formatLocalized(date, "h:mma", "p", locale),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return format(date, "MMM dd, yyyy, h:mma");
|
return formatLocalized(date, "MMM dd, yyyy, h:mma", "PPp", locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user