Skip to content

Commit 31647e8

Browse files
committed
fix: fork NuxtTime for i18n support
1 parent e46df53 commit 31647e8

File tree

1 file changed

+182
-68
lines changed

1 file changed

+182
-68
lines changed

app/components/DateTime.vue

Lines changed: 182 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,200 @@
11
<script setup lang="ts">
2-
/**
3-
* DateTime component that wraps NuxtTime with settings-aware relative date support.
4-
* Uses the global settings to determine whether to show relative or absolute dates.
5-
*
6-
* Note: When relativeDates setting is enabled, the component switches between
7-
* relative and absolute display based on user preference. The title attribute
8-
* always shows the full date for accessibility.
9-
*/
10-
const props = withDefaults(
11-
defineProps<{
12-
/** The datetime value (ISO string or Date) */
13-
datetime: string | Date
14-
/** Override title (defaults to datetime) */
15-
title?: string
16-
/** Date style for absolute display */
17-
dateStyle?: 'full' | 'long' | 'medium' | 'short'
18-
/** Individual date parts for absolute display (alternative to dateStyle) */
19-
year?: 'numeric' | '2-digit'
20-
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
21-
day?: 'numeric' | '2-digit'
22-
}>(),
23-
{
24-
title: undefined,
25-
dateStyle: undefined,
26-
year: undefined,
27-
month: undefined,
28-
day: undefined,
29-
},
30-
)
2+
interface NuxtTimeProps {
3+
datetime: string | number | Date
4+
localeMatcher?: 'best fit' | 'lookup'
5+
weekday?: 'long' | 'short' | 'narrow'
6+
era?: 'long' | 'short' | 'narrow'
7+
year?: 'numeric' | '2-digit'
8+
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
9+
day?: 'numeric' | '2-digit'
10+
hour?: 'numeric' | '2-digit'
11+
minute?: 'numeric' | '2-digit'
12+
second?: 'numeric' | '2-digit'
13+
timeZoneName?: 'short' | 'long' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric'
14+
formatMatcher?: 'best fit' | 'basic'
15+
hour12?: boolean
16+
timeZone?: string
3117
32-
const { locale } = useI18n()
18+
calendar?: string
19+
dayPeriod?: 'narrow' | 'short' | 'long'
20+
numberingSystem?: string
21+
22+
dateStyle?: 'full' | 'long' | 'medium' | 'short'
23+
timeStyle?: 'full' | 'long' | 'medium' | 'short'
24+
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
25+
26+
numeric?: 'always' | 'auto'
27+
relativeStyle?: 'long' | 'short' | 'narrow'
28+
}
29+
30+
const props = withDefaults(defineProps<NuxtTimeProps>(), {
31+
hour12: undefined,
32+
})
33+
34+
const el = getCurrentInstance()?.vnode.el
35+
const renderedDate = el?.getAttribute('datetime')
36+
const _locale = el?.getAttribute('data-locale')
37+
38+
const nuxtApp = useNuxtApp()
39+
40+
const date = computed(() => {
41+
const date = props.datetime
42+
if (renderedDate && nuxtApp.isHydrating) {
43+
return new Date(renderedDate)
44+
}
45+
if (!props.datetime) {
46+
return new Date()
47+
}
48+
return new Date(date)
49+
})
50+
51+
const now = ref(
52+
import.meta.client && nuxtApp.isHydrating && window._nuxtTimeNow
53+
? new Date(window._nuxtTimeNow)
54+
: new Date(),
55+
)
3356
3457
const relativeDates = useRelativeDates()
3558
36-
const dateFormatter = new Intl.DateTimeFormat(locale.value, {
59+
if (import.meta.client && relativeDates.value) {
60+
const handler = () => {
61+
now.value = new Date()
62+
}
63+
const interval = setInterval(handler, 1000)
64+
onBeforeUnmount(() => clearInterval(interval))
65+
}
66+
67+
const { locale } = useI18n()
68+
const defaults = {
3769
month: 'short',
3870
day: 'numeric',
3971
year: 'numeric',
4072
hour: 'numeric',
4173
minute: '2-digit',
4274
timeZoneName: 'short',
75+
}
76+
77+
const formatter = computed(() => {
78+
const { relativeStyle, ...rest } = props
79+
if (relativeDates.value) {
80+
return new Intl.RelativeTimeFormat(_locale ?? locale.value, {
81+
...defaults,
82+
...rest,
83+
style: relativeStyle,
84+
})
85+
}
86+
return new Intl.DateTimeFormat(_locale ?? locale.value, { ...defaults, ...rest })
4387
})
4488
45-
// Compute the title - always show full date for accessibility
46-
const titleValue = computed(() => {
47-
if (props.title) return props.title
48-
const date = typeof props.datetime === 'string' ? new Date(props.datetime) : props.datetime
49-
return dateFormatter.format(date)
89+
const formattedDate = computed(() => {
90+
if (!relativeDates.value) {
91+
return (formatter.value as Intl.DateTimeFormat).format(date.value)
92+
}
93+
94+
const diffInSeconds = (date.value.getTime() - now.value.getTime()) / 1000
95+
96+
const units: Array<{
97+
unit: Intl.RelativeTimeFormatUnit
98+
seconds: number
99+
threshold: number
100+
}> = [
101+
{ unit: 'second', seconds: 1, threshold: 60 }, // 60 seconds → minute
102+
{ unit: 'minute', seconds: 60, threshold: 60 }, // 60 minutes → hour
103+
{ unit: 'hour', seconds: 3600, threshold: 24 }, // 24 hours → day
104+
{ unit: 'day', seconds: 86400, threshold: 30 }, // ~30 days → month
105+
{ unit: 'month', seconds: 2592000, threshold: 12 }, // 12 months → year
106+
{ unit: 'year', seconds: 31536000, threshold: Infinity },
107+
]
108+
109+
const { unit, seconds } =
110+
units.find(({ seconds, threshold }) => Math.abs(diffInSeconds / seconds) < threshold) ||
111+
units[units.length - 1]!
112+
113+
const value = diffInSeconds / seconds
114+
return (formatter.value as Intl.RelativeTimeFormat).format(Math.round(value), unit)
50115
})
116+
117+
const isoDate = computed(() => date.value.toISOString())
118+
const dataset: Record<string, string | number | boolean | Date | undefined> = {}
119+
120+
if (import.meta.server) {
121+
for (const prop in props) {
122+
if (prop !== 'datetime') {
123+
const value = props?.[prop as keyof typeof props]
124+
if (value) {
125+
const propInKebabCase = prop.split(/(?=[A-Z])/).join('-')
126+
dataset[`data-${propInKebabCase}`] = props?.[prop as keyof typeof props]
127+
}
128+
}
129+
}
130+
onPrehydrate(el => {
131+
const now = (window._nuxtTimeNow ||= Date.now())
132+
const toCamelCase = (name: string, index: number) => {
133+
if (index > 0) {
134+
return name[0]!.toUpperCase() + name.slice(1)
135+
}
136+
return name
137+
}
138+
139+
const date = new Date(el.getAttribute('datetime')!)
140+
el.title = date.toISOString()
141+
142+
const options: Intl.DateTimeFormatOptions &
143+
Intl.RelativeTimeFormatOptions & { locale?: Intl.LocalesArgument; relative?: boolean } = {}
144+
for (const name of el.getAttributeNames()) {
145+
if (name.startsWith('data-')) {
146+
let optionName = name
147+
.slice(5)
148+
.split('-')
149+
.map(toCamelCase)
150+
.join('') as keyof (Intl.DateTimeFormatOptions & Intl.RelativeTimeFormatOptions)
151+
152+
if ((optionName as string) === 'relativeStyle') {
153+
optionName = 'style'
154+
}
155+
156+
options[optionName] = el.getAttribute(name) as any
157+
}
158+
}
159+
160+
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
161+
const relative = settings.relativeDates
162+
const locale = settings.selectedLocale
163+
164+
if (relative) {
165+
const diffInSeconds = (date.getTime() - now) / 1000
166+
const units: Array<{
167+
unit: Intl.RelativeTimeFormatUnit
168+
seconds: number
169+
threshold: number
170+
}> = [
171+
{ unit: 'second', seconds: 1, threshold: 60 }, // 60 seconds → minute
172+
{ unit: 'minute', seconds: 60, threshold: 60 }, // 60 minutes → hour
173+
{ unit: 'hour', seconds: 3600, threshold: 24 }, // 24 hours → day
174+
{ unit: 'day', seconds: 86400, threshold: 30 }, // ~30 days → month
175+
{ unit: 'month', seconds: 2592000, threshold: 12 }, // 12 months → year
176+
{ unit: 'year', seconds: 31536000, threshold: Infinity },
177+
]
178+
const { unit, seconds } =
179+
units.find(({ seconds, threshold }) => Math.abs(diffInSeconds / seconds) < threshold) ||
180+
units[units.length - 1]!
181+
const value = diffInSeconds / seconds
182+
const formatter = new Intl.RelativeTimeFormat(locale, options)
183+
el.textContent = formatter.format(Math.round(value), unit)
184+
} else {
185+
const formatter = new Intl.DateTimeFormat(locale, options)
186+
el.textContent = formatter.format(date)
187+
}
188+
})
189+
}
190+
191+
declare global {
192+
interface Window {
193+
_nuxtTimeNow?: number
194+
}
195+
}
51196
</script>
52197

53198
<template>
54-
<span>
55-
<ClientOnly>
56-
<NuxtTime
57-
v-if="relativeDates"
58-
:datetime="datetime"
59-
:title="titleValue"
60-
relative
61-
:locale="locale"
62-
/>
63-
<NuxtTime
64-
v-else
65-
:datetime="datetime"
66-
:title="titleValue"
67-
:date-style="dateStyle"
68-
:year="year"
69-
:month="month"
70-
:day="day"
71-
:locale="locale"
72-
/>
73-
<template #fallback>
74-
<NuxtTime
75-
:datetime="datetime"
76-
:title="titleValue"
77-
:date-style="dateStyle"
78-
:year="year"
79-
:month="month"
80-
:day="day"
81-
:locale="locale"
82-
/>
83-
</template>
84-
</ClientOnly>
85-
</span>
199+
<time v-bind="dataset" :datetime="isoDate" :title="isoDate">{{ formattedDate }}</time>
86200
</template>

0 commit comments

Comments
 (0)