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>
This commit is contained in:
Adam Kleizer
2024-11-23 18:33:46 +01:00
committed by GitHub
parent 6662bbd5b9
commit 3d972dcac3
7 changed files with 77 additions and 97 deletions

View File

@@ -3,13 +3,27 @@
<label class="label"> <label class="label">
<span class="label-text"> {{ label }} </span> <span class="label-text"> {{ label }} </span>
</label> </label>
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" /> <VueDatePicker
v-model="selected"
:enable-time-picker="false"
clearable
:dark="isDark"
:teleport="true"
:format="formatDate"
/>
</div> </div>
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4"> <div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<label class="label"> <label class="label">
<span class="label-text"> {{ label }} </span> <span class="label-text"> {{ label }} </span>
</label> </label>
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" /> <VueDatePicker
v-model="selected"
:enable-time-picker="false"
clearable
:dark="isDark"
:teleport="true"
:format="formatDate"
/>
</div> </div>
</template> </template>
@@ -38,6 +52,8 @@
const isDark = useIsDark(); const isDark = useIsDark();
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");
const selected = computed<Date | null>({ const selected = computed<Date | null>({
get() { get() {
// String // String

View File

@@ -22,6 +22,6 @@
return ""; return "";
} }
return fmtDate(props.date, props.format); return fmtDate(props.date, props.format, props.datetimeType);
}); });
</script> </script>

View File

@@ -1,5 +1,6 @@
import { useI18n } from "vue-i18n"; import { format, formatDistance } from "date-fns";
import { type UseTimeAgoMessages, type UseTimeAgoUnitNamesDefault } from "@vueuse/core"; /* eslint import/namespace: ['error', { allowComputed: true }] */
import * as Locales from "date-fns/locale";
const cache = { const cache = {
currency: "", currency: "",
@@ -20,105 +21,63 @@ export async function useFormatCurrency() {
} }
} }
return (value: number | string) => fmtCurrency(value, cache.currency); return (value: number | string) => fmtCurrency(value, cache.currency, getLocaleCode());
} }
export type DateTimeFormat = "relative" | "long" | "short" | "human"; export type DateTimeFormat = "relative" | "long" | "short" | "human";
export type DateTimeType = "date" | "time" | "datetime"; export type DateTimeType = "date" | "time" | "datetime";
function ordinalIndicator(num: number) { export function getLocaleCode() {
if (num > 3 && num < 21) return "th"; const { $i18nGlobal } = useNuxtApp();
switch (num % 10) { return ($i18nGlobal?.locale?.value as string) ?? "en-US";
case 1:
return "st";
case 2:
return "nd";
case 3:
return "rd";
default:
return "th";
}
} }
export function useLocaleTimeAgo(date: Date) { function getLocaleForDate() {
const { t } = useI18n(); const localeCode = getLocaleCode();
const lang = localeCode.length > 1 ? localeCode.substring(0, 2) : localeCode;
const I18N_MESSAGES: UseTimeAgoMessages<UseTimeAgoUnitNamesDefault> = { const region = localeCode.length > 2 ? localeCode.substring(3) : "";
justNow: t("components.global.date_time.just-now"), return Locales[(lang + region) as keyof typeof Locales] ?? Locales[lang as keyof typeof Locales] ?? Locales.enUS;
past: n => (n.match(/\d/) ? t("components.global.date_time.ago", [n]) : n),
future: n => (n.match(/\d/) ? t("components.global.date_time.in", [n]) : n),
month: (n, past) =>
n === 1
? past
? t("components.global.date_time.last-month")
: t("components.global.date_time.next-month")
: `${n} ${t(`components.global.date_time.months`)}`,
year: (n, past) =>
n === 1
? past
? t("components.global.date_time.last-year")
: t("components.global.date_time.next-year")
: `${n} ${t(`components.global.date_time.years`)}`,
day: (n, past) =>
n === 1
? past
? t("components.global.date_time.yesterday")
: t("components.global.date_time.tomorrow")
: `${n} ${t(`components.global.date_time.days`)}`,
week: (n, past) =>
n === 1
? past
? t("components.global.date_time.last-week")
: t("components.global.date_time.next-week")
: `${n} ${t(`components.global.date_time.weeks`)}`,
hour: n => `${n} ${n === 1 ? t("components.global.date_time.hour") : t("components.global.date_time.hours")}`,
minute: n => `${n} ${n === 1 ? t("components.global.date_time.minute") : t("components.global.date_time.minutes")}`,
second: n => `${n} ${n === 1 ? t("components.global.date_time.second") : t("components.global.date_time.seconds")}`,
invalid: "",
};
return useTimeAgo(date, {
fullDateFormatter: (date: Date) => date.toLocaleDateString(),
messages: I18N_MESSAGES,
});
} }
export function fmtDate(value: string | Date, fmt: DateTimeFormat = "human"): string { export function fmtDate(
const months = [ value: string | Date | number,
"January", fmt: DateTimeFormat = "human",
"February", type: DateTimeType = "date"
"March", ): string {
"April", const dt = typeof value === "string" || typeof value === "number" ? new Date(value) : value;
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const dt = typeof value === "string" ? new Date(value) : value; if (!dt || !validDate(dt)) {
if (!dt) {
return ""; return "";
} }
if (!validDate(dt)) { const localeOptions = { locale: getLocaleForDate() };
return "";
if (fmt === "relative") {
return `${formatDistance(dt, new Date(), { ...localeOptions, addSuffix: true })} (${fmtDate(dt, "short", "date")})`;
} }
if (type === "time") {
return format(dt, "p", localeOptions);
}
let formatStr = "";
switch (fmt) { switch (fmt) {
case "relative":
return useLocaleTimeAgo(dt).value + useDateFormat(dt, " (YYYY-MM-DD)").value;
case "long":
return useDateFormat(dt, "YYYY-MM-DD (dddd)").value;
case "short":
return useDateFormat(dt, "YYYY-MM-DD").value;
case "human": case "human":
// January 1st, 2021 formatStr = "PPP";
return `${months[dt.getMonth()]} ${dt.getDate()}${ordinalIndicator(dt.getDate())}, ${dt.getFullYear()}`; break;
case "long":
formatStr = "PP";
break;
case "short":
formatStr = "P";
break;
default: default:
return ""; return "";
} }
if (type === "datetime") {
formatStr += "p";
}
return format(dt, formatStr, localeOptions);
} }

View File

@@ -11,7 +11,9 @@ export function format(date: Date | string): string {
} }
export function zeroTime(date: Date): Date { export function zeroTime(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate()); return new Date(
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - date.getTimezoneOffset() * 60000
);
} }
export function factorRange(offset: number = 7): [Date, Date] { export function factorRange(offset: number = 7): [Date, Date] {

View File

@@ -99,16 +99,12 @@
let purchasePrice = 0; let purchasePrice = 0;
let soldPrice = 0; let soldPrice = 0;
let purchaseTime = null;
if (item.value.purchasePrice) { if (item.value.purchasePrice) {
purchasePrice = item.value.purchasePrice; purchasePrice = item.value.purchasePrice;
} }
if (item.value.soldPrice) { if (item.value.soldPrice) {
soldPrice = item.value.soldPrice; soldPrice = item.value.soldPrice;
} }
if (item.value.purchaseTime && typeof item.value.purchaseTime !== "string") {
purchaseTime = new Date(item.value.purchaseTime.getTime() - item.value.purchaseTime.getTimezoneOffset() * 60000);
}
console.log((item.value.purchasePrice ??= 0)); console.log((item.value.purchasePrice ??= 0));
console.log((item.value.soldPrice ??= 0)); console.log((item.value.soldPrice ??= 0));
@@ -121,7 +117,7 @@
assetId: item.value.assetId, assetId: item.value.assetId,
purchasePrice, purchasePrice,
soldPrice, soldPrice,
purchaseTime: purchaseTime as Date, purchaseTime: item.value.purchaseTime as Date,
}; };
const { error } = await api.items.update(itemId.value, payload); const { error } = await api.items.update(itemId.value, payload);

View File

@@ -8,6 +8,7 @@
import MdiFill from "~icons/mdi/fill"; import MdiFill from "~icons/mdi/fill";
import MdiPencil from "~icons/mdi/pencil"; import MdiPencil from "~icons/mdi/pencil";
import MdiAccountMultiple from "~icons/mdi/account-multiple"; import MdiAccountMultiple from "~icons/mdi/account-multiple";
import { getLocaleCode } from "~/composables/use-formatters";
definePageMeta({ definePageMeta({
middleware: ["auth"], middleware: ["auth"],
@@ -52,12 +53,11 @@
}); });
const currencyExample = computed(() => { const currencyExample = computed(() => {
const formatter = new Intl.NumberFormat("en-US", { return fmtCurrency(1000, currency.value?.code ?? "USD", getLocaleCode());
style: "currency", });
currency: currency.value ? currency.value.code : "USD",
});
return formatter.format(1000); const dateExample = computed(() => {
return fmtDate(new Date(Date.now() - 15 * 60000), "relative");
}); });
const { data: group } = useAsyncData(async () => { const { data: group } = useAsyncData(async () => {
@@ -389,6 +389,7 @@
{{ $t(`languages.${lang}`) }} ({{ $t(`languages.${lang}`, 1, { locale: lang }) }}) {{ $t(`languages.${lang}`) }} ({{ $t(`languages.${lang}`, 1, { locale: lang }) }})
</option> </option>
</select> </select>
<p class="m-2 text-sm">{{ $t("profile.example") }}: {{ $t("global.created") }} {{ dateExample }}</p>
</div> </div>
</BaseCard> </BaseCard>

View File

@@ -30,6 +30,12 @@ export default defineNuxtPlugin(({ vueApp }) => {
messages: messages(), messages: messages(),
}); });
vueApp.use(i18n); vueApp.use(i18n);
return {
provide: {
i18nGlobal: i18n.global,
},
};
}); });
export const messages = () => { export const messages = () => {