Skip to content

Commit 3d972dc

Browse files
adamkleizerÁdám Kleizer
and
Ádám Kleizer
authored
fix: #321 use date-fns for localized datetime formatting (#345)
* fix: #321 use date-fns for localized datetime formatting * chore: lint fixes for use-formatters * chore: more lint fixes for use-formatters * date and currency localization fixes --------- Co-authored-by: Ádám Kleizer <adkl@boyum-it.com>
1 parent 6662bbd commit 3d972dc

File tree

7 files changed

+77
-97
lines changed

7 files changed

+77
-97
lines changed

frontend/components/Form/DatePicker.vue

+18-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,27 @@
33
<label class="label">
44
<span class="label-text"> {{ label }} </span>
55
</label>
6-
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" />
6+
<VueDatePicker
7+
v-model="selected"
8+
:enable-time-picker="false"
9+
clearable
10+
:dark="isDark"
11+
:teleport="true"
12+
:format="formatDate"
13+
/>
714
</div>
815
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
916
<label class="label">
1017
<span class="label-text"> {{ label }} </span>
1118
</label>
12-
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" />
19+
<VueDatePicker
20+
v-model="selected"
21+
:enable-time-picker="false"
22+
clearable
23+
:dark="isDark"
24+
:teleport="true"
25+
:format="formatDate"
26+
/>
1327
</div>
1428
</template>
1529

@@ -38,6 +52,8 @@
3852
3953
const isDark = useIsDark();
4054
55+
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");
56+
4157
const selected = computed<Date | null>({
4258
get() {
4359
// String

frontend/components/global/DateTime.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@
2222
return "";
2323
}
2424
25-
return fmtDate(props.date, props.format);
25+
return fmtDate(props.date, props.format, props.datetimeType);
2626
});
2727
</script>
+42-83
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useI18n } from "vue-i18n";
2-
import { type UseTimeAgoMessages, type UseTimeAgoUnitNamesDefault } from "@vueuse/core";
1+
import { format, formatDistance } from "date-fns";
2+
/* eslint import/namespace: ['error', { allowComputed: true }] */
3+
import * as Locales from "date-fns/locale";
34

45
const cache = {
56
currency: "",
@@ -20,105 +21,63 @@ export async function useFormatCurrency() {
2021
}
2122
}
2223

23-
return (value: number | string) => fmtCurrency(value, cache.currency);
24+
return (value: number | string) => fmtCurrency(value, cache.currency, getLocaleCode());
2425
}
2526

2627
export type DateTimeFormat = "relative" | "long" | "short" | "human";
2728
export type DateTimeType = "date" | "time" | "datetime";
2829

29-
function ordinalIndicator(num: number) {
30-
if (num > 3 && num < 21) return "th";
31-
switch (num % 10) {
32-
case 1:
33-
return "st";
34-
case 2:
35-
return "nd";
36-
case 3:
37-
return "rd";
38-
default:
39-
return "th";
40-
}
30+
export function getLocaleCode() {
31+
const { $i18nGlobal } = useNuxtApp();
32+
return ($i18nGlobal?.locale?.value as string) ?? "en-US";
4133
}
4234

43-
export function useLocaleTimeAgo(date: Date) {
44-
const { t } = useI18n();
45-
46-
const I18N_MESSAGES: UseTimeAgoMessages<UseTimeAgoUnitNamesDefault> = {
47-
justNow: t("components.global.date_time.just-now"),
48-
past: n => (n.match(/\d/) ? t("components.global.date_time.ago", [n]) : n),
49-
future: n => (n.match(/\d/) ? t("components.global.date_time.in", [n]) : n),
50-
month: (n, past) =>
51-
n === 1
52-
? past
53-
? t("components.global.date_time.last-month")
54-
: t("components.global.date_time.next-month")
55-
: `${n} ${t(`components.global.date_time.months`)}`,
56-
year: (n, past) =>
57-
n === 1
58-
? past
59-
? t("components.global.date_time.last-year")
60-
: t("components.global.date_time.next-year")
61-
: `${n} ${t(`components.global.date_time.years`)}`,
62-
day: (n, past) =>
63-
n === 1
64-
? past
65-
? t("components.global.date_time.yesterday")
66-
: t("components.global.date_time.tomorrow")
67-
: `${n} ${t(`components.global.date_time.days`)}`,
68-
week: (n, past) =>
69-
n === 1
70-
? past
71-
? t("components.global.date_time.last-week")
72-
: t("components.global.date_time.next-week")
73-
: `${n} ${t(`components.global.date_time.weeks`)}`,
74-
hour: n => `${n} ${n === 1 ? t("components.global.date_time.hour") : t("components.global.date_time.hours")}`,
75-
minute: n => `${n} ${n === 1 ? t("components.global.date_time.minute") : t("components.global.date_time.minutes")}`,
76-
second: n => `${n} ${n === 1 ? t("components.global.date_time.second") : t("components.global.date_time.seconds")}`,
77-
invalid: "",
78-
};
79-
80-
return useTimeAgo(date, {
81-
fullDateFormatter: (date: Date) => date.toLocaleDateString(),
82-
messages: I18N_MESSAGES,
83-
});
35+
function getLocaleForDate() {
36+
const localeCode = getLocaleCode();
37+
const lang = localeCode.length > 1 ? localeCode.substring(0, 2) : localeCode;
38+
const region = localeCode.length > 2 ? localeCode.substring(3) : "";
39+
return Locales[(lang + region) as keyof typeof Locales] ?? Locales[lang as keyof typeof Locales] ?? Locales.enUS;
8440
}
8541

86-
export function fmtDate(value: string | Date, fmt: DateTimeFormat = "human"): string {
87-
const months = [
88-
"January",
89-
"February",
90-
"March",
91-
"April",
92-
"May",
93-
"June",
94-
"July",
95-
"August",
96-
"September",
97-
"October",
98-
"November",
99-
"December",
100-
];
101-
102-
const dt = typeof value === "string" ? new Date(value) : value;
103-
if (!dt) {
42+
export function fmtDate(
43+
value: string | Date | number,
44+
fmt: DateTimeFormat = "human",
45+
type: DateTimeType = "date"
46+
): string {
47+
const dt = typeof value === "string" || typeof value === "number" ? new Date(value) : value;
48+
49+
if (!dt || !validDate(dt)) {
10450
return "";
10551
}
10652

107-
if (!validDate(dt)) {
108-
return "";
53+
const localeOptions = { locale: getLocaleForDate() };
54+
55+
if (fmt === "relative") {
56+
return `${formatDistance(dt, new Date(), { ...localeOptions, addSuffix: true })} (${fmtDate(dt, "short", "date")})`;
57+
}
58+
59+
if (type === "time") {
60+
return format(dt, "p", localeOptions);
10961
}
11062

63+
let formatStr = "";
64+
11165
switch (fmt) {
112-
case "relative":
113-
return useLocaleTimeAgo(dt).value + useDateFormat(dt, " (YYYY-MM-DD)").value;
66+
case "human":
67+
formatStr = "PPP";
68+
break;
11469
case "long":
115-
return useDateFormat(dt, "YYYY-MM-DD (dddd)").value;
70+
formatStr = "PP";
71+
break;
11672
case "short":
117-
return useDateFormat(dt, "YYYY-MM-DD").value;
118-
case "human":
119-
// January 1st, 2021
120-
return `${months[dt.getMonth()]} ${dt.getDate()}${ordinalIndicator(dt.getDate())}, ${dt.getFullYear()}`;
73+
formatStr = "P";
74+
break;
12175
default:
12276
return "";
12377
}
78+
if (type === "datetime") {
79+
formatStr += "p";
80+
}
81+
82+
return format(dt, formatStr, localeOptions);
12483
}

frontend/lib/datelib/datelib.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export function format(date: Date | string): string {
1111
}
1212

1313
export function zeroTime(date: Date): Date {
14-
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
14+
return new Date(
15+
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - date.getTimezoneOffset() * 60000
16+
);
1517
}
1618

1719
export function factorRange(offset: number = 7): [Date, Date] {

frontend/pages/item/[id]/index/edit.vue

+1-5
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,12 @@
9999
100100
let purchasePrice = 0;
101101
let soldPrice = 0;
102-
let purchaseTime = null;
103102
if (item.value.purchasePrice) {
104103
purchasePrice = item.value.purchasePrice;
105104
}
106105
if (item.value.soldPrice) {
107106
soldPrice = item.value.soldPrice;
108107
}
109-
if (item.value.purchaseTime && typeof item.value.purchaseTime !== "string") {
110-
purchaseTime = new Date(item.value.purchaseTime.getTime() - item.value.purchaseTime.getTimezoneOffset() * 60000);
111-
}
112108
113109
console.log((item.value.purchasePrice ??= 0));
114110
console.log((item.value.soldPrice ??= 0));
@@ -121,7 +117,7 @@
121117
assetId: item.value.assetId,
122118
purchasePrice,
123119
soldPrice,
124-
purchaseTime: purchaseTime as Date,
120+
purchaseTime: item.value.purchaseTime as Date,
125121
};
126122
127123
const { error } = await api.items.update(itemId.value, payload);

frontend/pages/profile.vue

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import MdiFill from "~icons/mdi/fill";
99
import MdiPencil from "~icons/mdi/pencil";
1010
import MdiAccountMultiple from "~icons/mdi/account-multiple";
11+
import { getLocaleCode } from "~/composables/use-formatters";
1112
1213
definePageMeta({
1314
middleware: ["auth"],
@@ -52,12 +53,11 @@
5253
});
5354
5455
const currencyExample = computed(() => {
55-
const formatter = new Intl.NumberFormat("en-US", {
56-
style: "currency",
57-
currency: currency.value ? currency.value.code : "USD",
58-
});
56+
return fmtCurrency(1000, currency.value?.code ?? "USD", getLocaleCode());
57+
});
5958
60-
return formatter.format(1000);
59+
const dateExample = computed(() => {
60+
return fmtDate(new Date(Date.now() - 15 * 60000), "relative");
6161
});
6262
6363
const { data: group } = useAsyncData(async () => {
@@ -389,6 +389,7 @@
389389
{{ $t(`languages.${lang}`) }} ({{ $t(`languages.${lang}`, 1, { locale: lang }) }})
390390
</option>
391391
</select>
392+
<p class="m-2 text-sm">{{ $t("profile.example") }}: {{ $t("global.created") }} {{ dateExample }}</p>
392393
</div>
393394
</BaseCard>
394395

frontend/plugins/i18n.ts

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export default defineNuxtPlugin(({ vueApp }) => {
3030
messages: messages(),
3131
});
3232
vueApp.use(i18n);
33+
34+
return {
35+
provide: {
36+
i18nGlobal: i18n.global,
37+
},
38+
};
3339
});
3440

3541
export const messages = () => {

0 commit comments

Comments
 (0)