diff --git a/packages/frontend/core/src/components/affine/page-history-modal/data.ts b/packages/frontend/core/src/components/affine/page-history-modal/data.ts index 6635ef2969..b971cf780e 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/data.ts +++ b/packages/frontend/core/src/components/affine/page-history-modal/data.ts @@ -1,12 +1,9 @@ import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page'; -import { - type CalendarTranslation, - timestampToCalendarDate, -} from '@affine/core/utils'; import { DebugLogger } from '@affine/debug'; import type { ListHistoryQuery } from '@affine/graphql'; import { listHistoryQuery, recoverDocMutation } from '@affine/graphql'; +import { i18nTime } from '@affine/i18n'; import { assertEquals } from '@blocksuite/global/utils'; import { DocCollection } from '@blocksuite/store'; import { globalBlockSuiteSchema } from '@toeverything/infra'; @@ -175,13 +172,20 @@ export const useSnapshotPage = ( return page; }; -export const historyListGroupByDay = ( - histories: DocHistory[], - translation: CalendarTranslation -) => { +export const historyListGroupByDay = (histories: DocHistory[]) => { const map = new Map(); for (const history of histories) { - const day = timestampToCalendarDate(history.timestamp, translation); + const day = i18nTime(history.timestamp, { + relative: { + max: [1, 'week'], + accuracy: 'day', + weekday: true, + }, + absolute: { + accuracy: 'day', + noYear: true, + }, + }); const list = map.get(day) ?? []; list.push(history); map.set(day, list); diff --git a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx index 57626618d6..d4922a3c80 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx @@ -6,7 +6,7 @@ import { openSettingModalAtom } from '@affine/core/atoms'; import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; -import { Trans } from '@affine/i18n'; +import { i18nTime, Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons/rc'; import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store'; @@ -33,11 +33,7 @@ import { import { encodeStateAsUpdate } from 'yjs'; import { pageHistoryModalAtom } from '../../../atoms/page-history'; -import { - type CalendarTranslation, - mixpanel, - timestampToLocalTime, -} from '../../../utils'; +import { mixpanel } from '../../../utils'; import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor'; import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style'; import { @@ -144,7 +140,11 @@ const HistoryEditorPreview = ({
{title}
- {ts ? timestampToLocalTime(ts) : null} + {ts + ? i18nTime(ts, { + absolute: { accuracy: 'minute', noDate: true }, + }) + : null}
@@ -317,14 +317,8 @@ const PageHistoryList = ({ }) => { const t = useAFFiNEI18N(); const historyListByDay = useMemo(() => { - const translation: CalendarTranslation = { - yesterday: t['com.affine.yesterday'], - today: t['com.affine.today'], - tomorrow: t['com.affine.tomorrow'], - nextWeek: t['com.affine.nextWeek'], - }; - return historyListGroupByDay(historyList, translation); - }, [historyList, t]); + return historyListGroupByDay(historyList); + }, [historyList]); const [collapsedMap, setCollapsedMap] = useState>({}); @@ -382,7 +376,9 @@ const PageHistoryList = ({ data-active={activeVersion === history.timestamp} > {idx > list.length - 1 ? ( diff --git a/packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx b/packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx index fa5d63ce3b..6949d3bebe 100644 --- a/packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/property-row-value-renderer.tsx @@ -4,7 +4,7 @@ import type { PageInfoCustomPropertyMeta, PagePropertyType, } from '@affine/core/modules/properties/services/schema'; -import { timestampToLocalDate } from '@affine/core/utils'; +import { i18nTime } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { DocService, useService } from '@toeverything/infra'; import { noop } from 'lodash-es'; @@ -22,7 +22,7 @@ interface PropertyRowValueProps { export const DateValue = ({ property }: PropertyRowValueProps) => { const displayValue = property.value - ? timestampToLocalDate(property.value) + ? i18nTime(property.value, { absolute: { accuracy: 'day' } }) : undefined; const manager = useContext(managerContext); diff --git a/packages/frontend/core/src/components/affine/page-properties/table.tsx b/packages/frontend/core/src/components/affine/page-properties/table.tsx index 4b17c2e2ed..c9be1e5091 100644 --- a/packages/frontend/core/src/components/affine/page-properties/table.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/table.tsx @@ -14,11 +14,7 @@ import type { PageInfoCustomPropertyMeta, PagePropertyType, } from '@affine/core/modules/properties/services/schema'; -import { - timestampToHumanTime, - timestampToLocalDate, - timestampToLocalDateTime, -} from '@affine/core/utils'; +import { i18nTime } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; import { @@ -617,7 +613,7 @@ export const PagePropertiesTableHeader = ({ const timestampElement = useMemo(() => { const localizedCreateTime = manager.createDate - ? timestampToLocalDate(manager.createDate) + ? i18nTime(manager.createDate) : null; const createTimeElement = ( @@ -632,11 +628,11 @@ export const PagePropertiesTableHeader = ({ content={ <>
- {t['Updated']()} {timestampToLocalDateTime(serverClock)} + {t['Updated']()} {i18nTime(serverClock)}
{manager.createDate && (
- {t['Created']()} {timestampToLocalDateTime(manager.createDate)} + {t['Created']()} {i18nTime(manager.createDate)}
)} @@ -645,7 +641,16 @@ export const PagePropertiesTableHeader = ({
{!syncing && !retrying ? ( <> - {t['Updated']()} {timestampToHumanTime(serverClock)} + {t['Updated']()}{' '} + {i18nTime(serverClock, { + relative: { + max: [1, 'day'], + accuracy: 'minute', + }, + absolute: { + accuracy: 'day', + }, + })} ) : ( <>{t['com.affine.syncing']()} @@ -655,7 +660,7 @@ export const PagePropertiesTableHeader = ({ ) : manager.updatedDate ? (
- {t['Updated']()} {timestampToLocalDate(manager.updatedDate)} + {t['Updated']()} {i18nTime(manager.updatedDate)}
) : ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index 3940499ec2..8e23ac922c 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -18,7 +18,7 @@ import { SubscriptionRecurring, SubscriptionStatus, } from '@affine/graphql'; -import { Trans } from '@affine/i18n'; +import { i18nTime, Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ArrowRightSmallIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; @@ -29,11 +29,7 @@ import { openSettingModalAtom } from '../../../../../atoms'; import { useMutation } from '../../../../../hooks/use-mutation'; import { useQuery } from '../../../../../hooks/use-query'; import { SubscriptionService } from '../../../../../modules/cloud'; -import { - mixpanel, - popupWindow, - timestampToLocalDate, -} from '../../../../../utils'; +import { mixpanel, popupWindow } from '../../../../../utils'; import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary'; import { CancelAction, ResumeAction } from '../plans/actions'; import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions'; @@ -282,11 +278,13 @@ const AIPlanCard = ({ onClick }: { onClick: () => void }) => { /> ) : subscription?.nextBillAt ? ( t['com.affine.payment.ai.billing-tip.next-bill-at']({ - due: timestampToLocalDate(subscription.nextBillAt), + due: i18nTime(subscription.nextBillAt, { + absolute: { accuracy: 'day' }, + }), }) ) : subscription?.canceledAt && subscription.end ? ( t['com.affine.payment.ai.billing-tip.end-at']({ - end: timestampToLocalDate(subscription.end), + end: i18nTime(subscription.end, { absolute: { accuracy: 'day' } }), }) ) : null; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx index 5d277aebd8..cb62831fc8 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/ai-plan.tsx @@ -1,6 +1,6 @@ import { Button } from '@affine/component'; import { AuthService, SubscriptionService } from '@affine/core/modules/cloud'; -import { timestampToLocalDate } from '@affine/core/utils'; +import { i18nTime } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useService } from '@toeverything/infra'; import { useEffect } from 'react'; @@ -32,11 +32,15 @@ export const AIPlan = () => { const billingTip = subscription?.nextBillAt ? t['com.affine.payment.ai.billing-tip.next-bill-at']({ - due: timestampToLocalDate(subscription.nextBillAt), + due: i18nTime(subscription.nextBillAt, { + absolute: { accuracy: 'day' }, + }), }) : subscription?.canceledAt && subscription.end ? t['com.affine.payment.ai.billing-tip.end-at']({ - end: timestampToLocalDate(subscription.end), + end: i18nTime(subscription.end, { + absolute: { accuracy: 'day' }, + }), }) : null; diff --git a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx index b477994ade..3f962d0cfe 100644 --- a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx +++ b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx @@ -1,6 +1,7 @@ -import { Checkbox } from '@affine/component'; +import { Checkbox, Tooltip } from '@affine/component'; import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper'; import { TagService } from '@affine/core/modules/tag'; +import { i18nTime } from '@affine/i18n'; import { useDraggable } from '@dnd-kit/core'; import { useLiveData, useService } from '@toeverything/infra'; import type { PropsWithChildren } from 'react'; @@ -15,7 +16,7 @@ import { } from '../scoped-atoms'; import type { DraggableTitleCellData, PageListItemProps } from '../types'; import { useAllDocDisplayProperties } from '../use-all-doc-display-properties'; -import { ColWrapper, formatDate, stopPropagation } from '../utils'; +import { ColWrapper, stopPropagation } from '../utils'; import * as styles from './page-list-item.css'; import { PageTags } from './page-tags'; @@ -97,13 +98,17 @@ const PageCreateDateCell = ({ createDate, }: Pick) => { return ( -
- {formatDate(createDate)} -
+ +
+ {i18nTime(createDate, { + relative: true, + })} +
+
); }; @@ -111,13 +116,19 @@ const PageUpdatedDateCell = ({ updatedDate, }: Pick) => { return ( -
- {updatedDate ? formatDate(updatedDate) : '-'} -
+ +
+ {updatedDate + ? i18nTime(updatedDate, { + relative: true, + }) + : '-'} +
+
); }; diff --git a/packages/frontend/core/src/components/page-list/utils.tsx b/packages/frontend/core/src/components/page-list/utils.tsx index 2976813222..f91ce95054 100644 --- a/packages/frontend/core/src/components/page-list/utils.tsx +++ b/packages/frontend/core/src/components/page-list/utils.tsx @@ -5,70 +5,6 @@ import { forwardRef } from 'react'; import * as styles from './list.css'; import type { ColWrapperProps } from './types'; -export function isToday(date: Date): boolean { - const today = new Date(); - return ( - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear() - ); -} - -export function isYesterday(date: Date): boolean { - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - return ( - date.getFullYear() === yesterday.getFullYear() && - date.getMonth() === yesterday.getMonth() && - date.getDate() === yesterday.getDate() - ); -} - -export function isLastWeek(date: Date): boolean { - const today = new Date(); - const lastWeek = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() - 7 - ); - return date >= lastWeek && date < today; -} - -export function isLastMonth(date: Date): boolean { - const today = new Date(); - const lastMonth = new Date( - today.getFullYear(), - today.getMonth() - 1, - today.getDate() - ); - return date >= lastMonth && date < today; -} - -export function isLastYear(date: Date): boolean { - const today = new Date(); - const lastYear = new Date( - today.getFullYear() - 1, - today.getMonth(), - today.getDate() - ); - return date >= lastYear && date < today; -} - -export const formatDate = (date: Date): string => { - // yyyy-mm-dd MM-DD HH:mm - // const year = date.getFullYear(); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const day = date.getDate().toString().padStart(2, '0'); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - if (isToday(date)) { - // HH:mm - return `${hours}:${minutes}`; - } - // MM-DD HH:mm - return `${month}-${day} ${hours}:${minutes}`; -}; - export const ColWrapper = forwardRef( function ColWrapper( { diff --git a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx index a1a0ec8248..7a48b36d5c 100644 --- a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx +++ b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx @@ -4,6 +4,7 @@ import 'fake-indexeddb/auto'; import { WorkspacePropertiesAdapter } from '@affine/core/modules/properties'; +import { createI18n } from '@affine/i18n'; import { render } from '@testing-library/react'; import { FrameworkRoot, @@ -35,6 +36,7 @@ beforeEach(async () => { describe('useDocCollectionPageTitle', () => { test('basic', async () => { + createI18n(); const { framework, workspace, doc } = await configureTestingEnvironment(); const { findByText, rerender } = render( diff --git a/packages/frontend/core/src/hooks/use-journal.ts b/packages/frontend/core/src/hooks/use-journal.ts index 33759a50c0..c419988569 100644 --- a/packages/frontend/core/src/hooks/use-journal.ts +++ b/packages/frontend/core/src/hooks/use-journal.ts @@ -1,9 +1,9 @@ +import { i18nTime } from '@affine/i18n'; import { initEmptyPage } from '@toeverything/infra'; import dayjs from 'dayjs'; import { useCallback, useMemo } from 'react'; import type { DocCollection } from '../shared'; -import { timestampToLocalDate } from '../utils'; import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter'; import { useDocCollectionHelper } from './use-block-suite-workspace-helper'; import { useNavigateHelper } from './use-navigate-helper'; @@ -109,7 +109,7 @@ export const useJournalHelper = (docCollection: DocCollection) => { (pageId: string) => { const journalDateString = getJournalDateString(pageId); if (!journalDateString) return null; - return timestampToLocalDate(journalDateString); + return i18nTime(journalDateString, { absolute: { accuracy: 'day' } }); }, [getJournalDateString] ); diff --git a/packages/frontend/core/src/modules/cmdk/views/main.tsx b/packages/frontend/core/src/modules/cmdk/views/main.tsx index 6250cce249..810ad90f8a 100644 --- a/packages/frontend/core/src/modules/cmdk/views/main.tsx +++ b/packages/frontend/core/src/modules/cmdk/views/main.tsx @@ -1,9 +1,9 @@ import { Loading } from '@affine/component/ui/loading'; import type { CommandCategory } from '@affine/core/commands'; -import { formatDate } from '@affine/core/components/page-list'; import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { QuickSearchService } from '@affine/core/modules/cmdk'; +import { i18nTime } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { DocMeta } from '@blocksuite/store'; import { useLiveData, useService } from '@toeverything/infra'; @@ -111,7 +111,7 @@ const QuickSearchGroup = ({
{command.timestamp ? (
- {formatDate(new Date(command.timestamp))} + {i18nTime(command.timestamp, { relative: true })}
) : null} {command.keyBinding ? ( diff --git a/packages/frontend/core/src/utils/__tests__/intl-formatter.spec.ts b/packages/frontend/core/src/utils/__tests__/intl-formatter.spec.ts deleted file mode 100644 index aea8a6f974..0000000000 --- a/packages/frontend/core/src/utils/__tests__/intl-formatter.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { getI18n } from '@affine/i18n'; -import { describe, expect, test } from 'vitest'; - -import type { CalendarTranslation } from '../intl-formatter'; -import { timestampToCalendarDate } from '../intl-formatter'; - -const translation: CalendarTranslation = { - yesterday: () => 'Yesterday', - today: () => 'Today', - tomorrow: () => 'Tomorrow', - nextWeek: () => 'Next Week', -}; - -const ONE_DAY = 24 * 60 * 60 * 1000; - -describe('intl calendar date formatter', () => { - const week = new Intl.DateTimeFormat(getI18n()?.language, { - weekday: 'long', - }); - - test('someday before last week', async () => { - const timestamp = '2000-01-01 10:00'; - expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 2000'); - }); - - test('someday in last week', async () => { - const timestamp = Date.now() - 6 * ONE_DAY; - expect(timestampToCalendarDate(timestamp, translation)).toBe( - week.format(timestamp) - ); - }); - - test('someday is yesterday', async () => { - const timestamp = Date.now() - ONE_DAY; - expect(timestampToCalendarDate(timestamp, translation)).toBe('Yesterday'); - }); - - test('someday is today', async () => { - const timestamp = Date.now(); - expect(timestampToCalendarDate(timestamp, translation)).toBe('Today'); - }); - - test('someday is tomorrow', async () => { - const timestamp = Date.now() + ONE_DAY; - expect(timestampToCalendarDate(timestamp, translation)).toBe('Tomorrow'); - }); - - test('someday in next week', async () => { - const timestamp = Date.now() + 6 * ONE_DAY; - expect(timestampToCalendarDate(timestamp, translation)).toBe( - `Next Week ${week.format(timestamp)}` - ); - }); - - test('someday after next week', async () => { - const timestamp = '3000-01-01 10:00'; - expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 3000'); - }); -}); - -describe('intl calendar date formatter with specific reference time', () => { - const referenceTime = '2024-05-10 14:00'; - - test('someday before last week', async () => { - const timestamp = '2024-04-27 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Apr 27, 2024' - ); - }); - - test('someday in last week', async () => { - const timestamp = '2024-05-6 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Monday' - ); - }); - - test('someday is yesterday', async () => { - const timestamp = '2024-05-9 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Yesterday' - ); - }); - - test('someday is today', async () => { - const timestamp = '2024-05-10 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Today' - ); - }); - - test('someday is tomorrow', async () => { - const timestamp = '2024-05-11 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Tomorrow' - ); - }); - - test('someday in next week', async () => { - const timestamp = '2024-05-15 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'Next Week Wednesday' - ); - }); - - test('someday after next week', async () => { - const timestamp = '2024-05-30 10:00'; - expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe( - 'May 30, 2024' - ); - }); -}); diff --git a/packages/frontend/core/src/utils/index.ts b/packages/frontend/core/src/utils/index.ts index 07e73f5141..7505a714a2 100644 --- a/packages/frontend/core/src/utils/index.ts +++ b/packages/frontend/core/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './create-emotion-cache'; export * from './fractional-indexing'; -export * from './intl-formatter'; export * from './mixpanel'; export * from './popup'; export * from './string2color'; diff --git a/packages/frontend/core/src/utils/intl-formatter.ts b/packages/frontend/core/src/utils/intl-formatter.ts deleted file mode 100644 index d83dbe8bcf..0000000000 --- a/packages/frontend/core/src/utils/intl-formatter.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getI18n } from '@affine/i18n'; -import dayjs from 'dayjs'; - -function createTimeFormatter() { - return new Intl.DateTimeFormat(getI18n()?.language, { - timeStyle: 'short', - }); -} - -function createDateTimeFormatter() { - return new Intl.DateTimeFormat(getI18n()?.language, { - timeStyle: 'medium', - dateStyle: 'medium', - }); -} - -function createDateFormatter() { - return new Intl.DateTimeFormat(getI18n()?.language, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); -} - -function createWeekFormatter() { - return new Intl.DateTimeFormat(getI18n()?.language, { - weekday: 'long', - }); -} - -export const timestampToLocalTime = (ts: string | number) => { - const formatter = createTimeFormatter(); - return formatter.format(dayjs(ts).toDate()); -}; - -export const timestampToLocalDate = (ts: string | number) => { - const formatter = createDateFormatter(); - return formatter.format(dayjs(ts).toDate()); -}; - -export const timestampToLocalDateTime = (ts: string | number) => { - const formatter = createDateTimeFormatter(); - return formatter.format(dayjs(ts).toDate()); -}; - -export const createRelativeTimeFormatter = () => { - return new Intl.RelativeTimeFormat(getI18n()?.language, { - style: 'narrow', - }); -}; - -export interface CalendarTranslation { - yesterday: () => string; - today: () => string; - tomorrow: () => string; - nextWeek: () => string; -} - -export const timestampToCalendarDate = ( - ts: string | number, - translation: CalendarTranslation, - referenceTime?: string | number -) => { - const startOfDay = dayjs(referenceTime).startOf('d'); - const diff = dayjs(ts).diff(startOfDay, 'd', true); - const sameElse = timestampToLocalDate(ts); - - const formatter = createWeekFormatter(); - const week = formatter.format(dayjs(ts).toDate()); - - return diff < -6 - ? sameElse - : diff < -1 - ? week - : diff < 0 - ? translation.yesterday() - : diff < 1 - ? translation.today() - : diff < 2 - ? translation.tomorrow() - : diff < 7 - ? `${translation.nextWeek()} ${week}` - : sameElse; -}; - -// TODO: refactor this to @affine/i18n -export const timestampToHumanTime = (ts: number) => { - const diff = Math.abs(dayjs(ts).diff(dayjs())); - - if (diff < 1000 * 60) { - return getI18n().t('com.affine.just-now'); - } else if (diff < 1000 * 60 * 60) { - return createRelativeTimeFormatter().format( - -Math.floor(diff / 1000 / 60), - 'minutes' - ); - } else if (diff < 1000 * 60 * 60 * 24) { - return createRelativeTimeFormatter().format( - -Math.floor(diff / 1000 / 60 / 60), - 'hours' - ); - } else { - return timestampToLocalDate(ts); - } -}; diff --git a/packages/frontend/i18n/package.json b/packages/frontend/i18n/package.json index 99c758e73c..83bb04d01d 100644 --- a/packages/frontend/i18n/package.json +++ b/packages/frontend/i18n/package.json @@ -7,13 +7,6 @@ ".": "./src/index.ts", "./hooks": "./src/i18n-generated" }, - "publishConfig": { - "main": "dist/src/index.js", - "types": "dist/src/index.d.ts", - "exports": { - ".": "./dist/src/index.js" - } - }, "scripts": { "build": "node build.mjs", "dev": "node dev.mjs", @@ -28,6 +21,7 @@ }, "dependencies": { "@magic-works/i18n-codegen": "^0.6.0", + "dayjs": "^1.11.11", "i18next": "^23.11.1", "react-i18next": "^14.1.0", "undici": "^6.12.0" @@ -36,7 +30,8 @@ "@types/prettier": "^3.0.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "vitest": "1.6.0" }, "version": "0.14.0" } diff --git a/packages/frontend/i18n/src/i18n.ts b/packages/frontend/i18n/src/i18n.ts new file mode 100644 index 0000000000..e4a024f5e0 --- /dev/null +++ b/packages/frontend/i18n/src/i18n.ts @@ -0,0 +1,54 @@ +import { getI18n } from 'react-i18next'; + +import type { useAFFiNEI18N } from './i18n-generated'; + +export type I18nFuncs = ReturnType; + +export type I18nInfos = { + [K in keyof I18nFuncs]: I18nFuncs[K] extends (...a: infer Opt) => any + ? Opt[0] + : never; +}; + +export type I18nKeys = keyof I18nInfos; + +export type I18nString = + | { + [K in I18nKeys]: { + key: K; + } & (I18nInfos[K] extends undefined + ? unknown + : { options: I18nInfos[K] }); + }[I18nKeys] + | string; + +const I18nMethod = { + t(i18nStr: I18nString) { + const i18n = getI18n(); + if (typeof i18nStr === 'object') { + return i18n.t(i18nStr.key, 'options' in i18nStr ? i18nStr.options : {}); + } + return i18nStr; + }, + get language() { + const i18n = getI18n(); + return i18n.language; + }, +}; + +const I18nProxy = new Proxy(I18nMethod, { + get(self, key) { + const i18n = getI18n(); + if (typeof key === 'string' && i18n.exists(key)) { + return i18n.t.bind(i18n, key as string); + } else { + return (self as any)[key as string] as any; + } + }, +}); + +/** + * I18n['com.affine.xxx']({ arg1: 'hello' }) -> '中文 hello' + */ +export const I18n = I18nProxy as I18nFuncs & typeof I18nMethod; +export type I18n = typeof I18n; diff --git a/packages/frontend/i18n/src/index.ts b/packages/frontend/i18n/src/index.ts index 3866cbb81f..3e611aa7e5 100644 --- a/packages/frontend/i18n/src/index.ts +++ b/packages/frontend/i18n/src/index.ts @@ -11,6 +11,10 @@ import { import { LOCALES } from './resources'; import type en_US from './resources/en.json'; +export * from './i18n'; +export * from './i18n-generated'; +export * from './utils'; + declare module 'i18next' { // Refs: https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz interface CustomTypeOptions { @@ -88,9 +92,11 @@ export const createI18n = (): I18nextProviderProps['i18n'] => { console.error('i18n init failed'); }); - i18n.on('languageChanged', lng => { - localStorage.setItem(STORAGE_KEY, lng); - }); + if (globalThis.localStorage) { + i18n.on('languageChanged', lng => { + localStorage.setItem(STORAGE_KEY, lng); + }); + } return i18n; }; export function setUpLanguage(i: i18n) { diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 2707a316e2..c4200b9504 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1311,7 +1311,11 @@ "com.affine.workspaceType.offline": "Available Offline", "com.affine.write_with_a_blank_page": "Write with a blank page", "com.affine.yesterday": "Yesterday", - "com.affine.just-now": "Just now", + "com.affine.time.now": "now", + "com.affine.time.this-week": "this week", + "com.affine.time.this-mouth": "this month", + "com.affine.time.this-year": "this year", + "com.affine.time.today": "today", "com.affine.syncing": "Syncing", "core": "core", "dark": "Dark", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index b825d52fb5..1c84671242 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -1312,6 +1312,11 @@ "com.affine.workspaceType.offline": "离线可用", "com.affine.write_with_a_blank_page": "在空白页面书写", "com.affine.yesterday": "昨天", + "com.affine.time.now": "刚刚", + "com.affine.time.this-week": "本周", + "com.affine.time.this-mouth": "本月", + "com.affine.time.this-year": "今年", + "com.affine.time.today": "今天", "core": "核心", "dark": "深色", "emptyAllPages": "点击<1>$t(New Page)按钮创建您的第一篇文档。", diff --git a/packages/frontend/i18n/src/utils/__tests__/time.spec.ts b/packages/frontend/i18n/src/utils/__tests__/time.spec.ts new file mode 100644 index 0000000000..192f1d9678 --- /dev/null +++ b/packages/frontend/i18n/src/utils/__tests__/time.spec.ts @@ -0,0 +1,399 @@ +import { describe, expect, test } from 'vitest'; + +import { createI18n, getI18n } from '../../'; +import { i18nTime } from '../time'; + +// Intl api is not available in github action, skip the test +describe.skip('humanTime', () => { + test('absolute', async () => { + createI18n(); + expect(i18nTime('2024-10-10 13:30:28')).toBe('Oct 10, 2024, 1:30:28 PM'); + expect( + i18nTime('2024-10-10 13:30:28', { + absolute: { + accuracy: 'minute', + }, + }) + ).toBe('Oct 10, 2024, 1:30 PM'); + expect( + i18nTime('2024-10-10 13:30:28', { + absolute: { + accuracy: 'day', + }, + }) + ).toBe('Oct 10, 2024'); + expect( + i18nTime('2024-10-10 13:30:28', { + absolute: { + accuracy: 'day', + noYear: true, + }, + }) + ).toBe('Oct 10'); + expect( + i18nTime('2024-10-10 13:30:28', { + absolute: { + accuracy: 'year', + }, + }) + ).toBe('2024'); + expect( + i18nTime('2024-10-10 13:30:28', { + absolute: { + noDate: true, + accuracy: 'minute', + }, + }) + ).toBe('1:30 PM'); + }); + + test('relative', async () => { + createI18n(); + expect( + i18nTime('2024-10-10 13:30:28.005', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('1s ago'); + expect( + i18nTime('2024-10-10 13:25:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('5m ago'); + expect( + i18nTime('2024-10-10 12:59:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('31m ago'); + expect( + i18nTime('2024-10-10 12:30:30', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('1h ago'); + expect( + i18nTime('2024-10-9 13:30:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('yesterday'); + expect( + i18nTime('2024-10-9 12:30:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('yesterday'); + expect( + i18nTime('2024-10-8 23:59:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('2d ago'); + expect( + i18nTime('2024-10-7 23:59:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('3d ago'); + expect( + i18nTime('2024-10-4 00:00:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('6d ago'); + expect( + i18nTime('2024-10-3 23:59:59', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('last wk.'); + expect( + i18nTime('2024-9-29 23:59:59', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('last wk.'); + expect( + i18nTime('2024-9-28 23:59:59', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('2w ago'); + expect( + i18nTime('2024-9-15 00:00:00', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('last mo.'); + expect( + i18nTime('2024-9-1 00:00:00', { + now: '2024-9-30 13:30:30', + relative: true, + }) + ).toBe('4w ago'); + expect( + i18nTime('2024-9-10 13:30:30', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('last mo.'); + expect( + i18nTime('2023-9-10 13:30:30', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('last yr.'); + }); + + test('relative - accuracy', async () => { + createI18n(); + expect( + i18nTime('2024-10-10 13:30:28.005', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'minute', + }, + }) + ).toBe('now'); + expect( + i18nTime('2024-10-10 13:25:00', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'minute', + }, + }) + ).toBe('5m ago'); + expect( + i18nTime('2024-10-10 12:59:00', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'hour', + }, + }) + ).toBe('now'); + expect( + i18nTime('2024-10-10 12:30:30', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'day', + }, + }) + ).toBe('today'); + expect( + i18nTime('2024-10-4 00:00:00', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'week', + }, + }) + ).toBe('last wk.'); + expect( + i18nTime('2024-10-9 00:00:00', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'week', + }, + }) + ).toBe('this week'); + expect( + i18nTime('2024-9-1 00:00:00', { + now: '2024-9-30 13:30:30', + relative: { + accuracy: 'month', + }, + }) + ).toBe('this month'); + expect( + i18nTime('2024-9-10 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'year', + }, + }) + ).toBe('this year'); + expect( + i18nTime('2023-9-10 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'year', + }, + }) + ).toBe('last yr.'); + }); + + test('relative - disable yesterdayAndTomorrow', async () => { + createI18n(); + expect( + i18nTime('2024-10-9 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + yesterdayAndTomorrow: false, + }, + }) + ).toBe('1d ago'); + expect( + i18nTime('2024-10-11 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + yesterdayAndTomorrow: false, + }, + }) + ).toBe('in 1d'); + }); + + test('relative - weekday', async () => { + createI18n(); + expect( + i18nTime('2024-10-9 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('Wednesday'); + expect( + i18nTime('2024-10-4 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('Friday'); + expect( + i18nTime('2024-10-3 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('1w ago'); + expect( + i18nTime('2024-10-11 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('Friday'); + expect( + i18nTime('2024-10-16 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('Wednesday'); + expect( + i18nTime('2024-10-17 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + yesterdayAndTomorrow: false, + }, + }) + ).toBe('in 1w'); + }); + + test('mix relative and absolute', async () => { + createI18n(); + expect( + i18nTime('2024-10-9 14:30:30', { + now: '2024-10-10 13:30:30', + relative: { + max: [1, 'day'], + }, + }) + ).toBe('23h ago'); + expect( + i18nTime('2024-10-9 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + max: [1, 'day'], + }, + absolute: { + accuracy: 'day', + }, + }) + ).toBe('Oct 9, 2024'); + + expect( + i18nTime('2024-10-9 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + max: [2, 'day'], + }, + absolute: { + accuracy: 'day', + }, + }) + ).toBe('yesterday'); + + expect( + i18nTime('2024-10-8 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + max: [2, 'day'], + }, + absolute: { + accuracy: 'day', + }, + }) + ).toBe('Oct 8, 2024'); + }); + + test('chinese', () => { + createI18n(); + getI18n().changeLanguage('zh-Hans'); + expect(i18nTime('2024-10-10 13:30:28.005')).toBe('2024年10月10日 13:30:28'); + expect( + i18nTime('2024-10-10 13:30:28.005', { + absolute: { + accuracy: 'day', + }, + }) + ).toBe('2024年10月10日'); + expect( + i18nTime('2024-10-10 13:30:28.005', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('1秒前'); + expect( + i18nTime('2024-10-9 13:30:30', { + now: '2024-10-10 13:30:30', + relative: true, + }) + ).toBe('昨天'); + expect( + i18nTime('2024-10-8 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + weekday: true, + }, + }) + ).toBe('星期二'); + expect( + i18nTime('2024-10-8 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'week', + }, + }) + ).toBe('本周'); + expect( + i18nTime('2024-10-8 13:30:30', { + now: '2024-10-10 13:30:30', + relative: { + accuracy: 'month', + }, + }) + ).toBe('本月'); + }); +}); diff --git a/packages/frontend/i18n/src/utils/index.ts b/packages/frontend/i18n/src/utils/index.ts new file mode 100644 index 0000000000..8aae78294a --- /dev/null +++ b/packages/frontend/i18n/src/utils/index.ts @@ -0,0 +1 @@ +export { i18nTime } from './time'; diff --git a/packages/frontend/i18n/src/utils/time.ts b/packages/frontend/i18n/src/utils/time.ts new file mode 100644 index 0000000000..4d7e015471 --- /dev/null +++ b/packages/frontend/i18n/src/utils/time.ts @@ -0,0 +1,281 @@ +import dayjs from 'dayjs'; + +import { I18n } from '../i18n'; + +export type TimeUnit = + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'year'; + +const timeUnitCode = { + second: 1, + minute: 2, + hour: 3, + day: 4, + week: 5, + month: 6, + year: 7, +} satisfies Record; + +/** + * ```ts + * // timestamp to string + * i18nTime(1728538228000) -> 'Oct 10, 2024, 1:30:28 PM' + * + * // absolute time string + * i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute' } }) -> '2024-10-10 13:30 PM' + * i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute', noDate: true } }) -> '13:30 PM' + * i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'minute', noYear: true } }) -> 'Oct 10, 13:30 PM' + * i18nTime('2024-10-10 13:30:28', { absolute: { accuracy: 'day' } }) -> 'Oct 10, 2024' + * + * // relative time string + * i18nTime('2024-10-10 13:30:30', { relative: true }) -> 'now' + * i18nTime('2024-10-10 13:30:00', { relative: true }) -> '30s ago' + * i18nTime('2024-10-10 13:30:30', { relative: { accuracy: 'minute' } }) -> '2m ago' + * + * // show absolute time string if time is pass 1 day + * i18nTime('2024-10-9 14:30:30', { relative: { max: [1, 'day'] } }) -> '23h ago' + * i18nTime('2024-10-9 13:30:30', { relative: { max: [1, 'day'] } }) -> 'Oct 9, 2024, 1:30:30 PM' + * ``` + */ +export function i18nTime( + time: dayjs.ConfigType, + options: { + // override i18n instance, default is global I18n instance + i18n?: I18n; + // override now time, default is current time + now?: dayjs.ConfigType; + relative?: + | { + // max time to show relative time, if time is pass this time, show absolute time + max?: [number, TimeUnit]; + // show time with this accuracy + accuracy?: TimeUnit; + // show weekday, e.g. 'Monday', 'Tuesday', etc. + weekday?: boolean; + // show 'yesterday' or 'tomorrow' if time is + yesterdayAndTomorrow?: boolean; + } + | true; // use default relative option + absolute?: { + // show time with this accuracy + accuracy?: TimeUnit; + // hide year + noYear?: boolean; + // hide date (year, month, day) + noDate?: boolean; + }; + } = {} +) { + const i18n = options.i18n ?? I18n; + time = dayjs(time); + const now = dayjs(options.now); + + const defaultRelativeOption = { + max: [1000, 'year'], + accuracy: 'second', + weekday: false, + yesterdayAndTomorrow: true, + } satisfies typeof options.relative; + + const relativeOption = options.relative + ? options.relative === true + ? defaultRelativeOption + : { + ...defaultRelativeOption, + ...options.relative, + } + : null; + + const defaultAbsoluteOption = { + accuracy: 'second', + noYear: false, + noDate: false, + } satisfies typeof options.absolute; + + const absoluteOption = { + ...defaultAbsoluteOption, + ...options.absolute, + }; + + if (relativeOption) { + // show relative + + const formatter = new Intl.RelativeTimeFormat(i18n.language, { + style: 'narrow', + numeric: relativeOption.yesterdayAndTomorrow ? 'auto' : 'always', + }); + + const timeUnitProcessor = { + second: () => { + const diffSecond = time.diff(now) / 1000; + if (Math.abs(diffSecond) < 1) { + return i18n['com.affine.time.now'](); + } + if ( + relativeOption.max[1] === 'second' && + Math.abs(diffSecond) >= relativeOption.max[0] + ) { + return false; + } + if (Math.abs(diffSecond) < 60) { + return formatter.format(Math.trunc(diffSecond), 'second'); + } + return null; + }, + minute: () => { + const diffMinute = time.diff(now) / 1000 / 60; + if (Math.abs(diffMinute) < 1) { + return i18n['com.affine.time.now'](); + } + if ( + relativeOption.max[1] === 'minute' && + Math.abs(diffMinute) >= relativeOption.max[0] + ) { + return false; + } + if (Math.abs(diffMinute) < 60) { + return formatter.format(Math.trunc(diffMinute), 'minute'); + } + return null; + }, + hour: () => { + const diffHour = time.diff(now) / 1000 / 60 / 60; + if (Math.abs(diffHour) < 1) { + return i18n['com.affine.time.now'](); + } + if ( + relativeOption.max[1] === 'hour' && + Math.abs(diffHour) >= relativeOption.max[0] + ) { + return false; + } + if (Math.abs(diffHour) < 24) { + return formatter.format(Math.trunc(diffHour), 'hour'); + } + return null; + }, + day: () => { + const diffDay = time.startOf('day').diff(now.startOf('day'), 'day'); + if (Math.abs(diffDay) < 1) { + return i18n['com.affine.time.today'](); + } + if ( + relativeOption.max[1] === 'day' && + Math.abs(diffDay) >= relativeOption.max[0] + ) { + return false; + } + if (relativeOption.yesterdayAndTomorrow && Math.abs(diffDay) < 2) { + return formatter.format(Math.trunc(diffDay), 'day'); + } else if (relativeOption.weekday && Math.abs(diffDay) < 7) { + return new Intl.DateTimeFormat(i18n.language, { + weekday: 'long', + }).format(time.startOf('day').toDate()); + } else if (Math.abs(diffDay) < 7) { + return formatter.format(Math.trunc(diffDay), 'day'); + } + return null; + }, + week: () => { + const inSameMonth = time.startOf('month').isSame(now.startOf('month')); + const diffWeek = time.startOf('week').diff(now.startOf('week'), 'week'); + if (Math.abs(diffWeek) < 1) { + return i18n['com.affine.time.this-week'](); + } + if ( + relativeOption.max[1] === 'week' && + Math.abs(diffWeek) >= relativeOption.max[0] + ) { + return false; + } + if (inSameMonth || Math.abs(diffWeek) < 3) { + return formatter.format(Math.trunc(diffWeek), 'week'); + } + return null; + }, + month: () => { + const diffMonth = time + .startOf('month') + .diff(now.startOf('month'), 'month'); + if (Math.abs(diffMonth) < 1) { + return i18n['com.affine.time.this-mouth'](); + } + if ( + relativeOption.max[1] === 'month' && + Math.abs(diffMonth) >= relativeOption.max[0] + ) { + return false; + } + if (Math.abs(diffMonth) < 12) { + return formatter.format(Math.trunc(diffMonth), 'month'); + } + return null; + }, + year: () => { + const diffYear = time.startOf('year').diff(now.startOf('year'), 'year'); + if (Math.abs(diffYear) < 1) { + return i18n['com.affine.time.this-year'](); + } + if ( + relativeOption.max[1] === 'year' && + Math.abs(diffYear) >= relativeOption.max[0] + ) { + return false; + } + return formatter.format(Math.trunc(diffYear), 'year'); + }, + } as Record string | false | null>; + + const processors = Object.entries(timeUnitProcessor).sort( + (a, b) => timeUnitCode[a[0] as TimeUnit] - timeUnitCode[b[0] as TimeUnit] + ) as [TimeUnit, () => string | false | null][]; + + for (const [unit, processor] of processors) { + if (timeUnitCode[relativeOption.accuracy] > timeUnitCode[unit]) { + continue; + } + const result = processor(); + if (result) { + return result; + } + if (result === false) { + break; + } + } + } + + // show absolute + const formatter = new Intl.DateTimeFormat(i18n.language, { + year: + !absoluteOption.noYear && !absoluteOption.noDate ? 'numeric' : undefined, + month: + !absoluteOption.noDate && + timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['month'] + ? 'short' + : undefined, + day: + !absoluteOption.noDate && + timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['day'] + ? 'numeric' + : undefined, + hour: + timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['hour'] + ? 'numeric' + : undefined, + minute: + timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['minute'] + ? 'numeric' + : undefined, + second: + timeUnitCode[absoluteOption.accuracy] <= timeUnitCode['second'] + ? 'numeric' + : undefined, + }); + + return formatter.format(time.toDate()); +} diff --git a/yarn.lock b/yarn.lock index 1f379ee96f..e7bb4104e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -611,12 +611,14 @@ __metadata: dependencies: "@magic-works/i18n-codegen": "npm:^0.6.0" "@types/prettier": "npm:^3.0.0" + dayjs: "npm:^1.11.11" i18next: "npm:^23.11.1" prettier: "npm:^3.2.5" react-i18next: "npm:^14.1.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.4.5" undici: "npm:^6.12.0" + vitest: "npm:1.6.0" languageName: unknown linkType: soft @@ -20240,7 +20242,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.10": +"dayjs@npm:^1.11.10, dayjs@npm:^1.11.11": version: 1.11.11 resolution: "dayjs@npm:1.11.11" checksum: 10/f03948b172fbeed229837965988d1d5bac99c72a31c28731a457303259439f2f36289186489ae140adbeb10f591a926908c8de5d81eb449a2edbf5cbd6e9e30c