();
const username = computed(() => props.socialAccount.username || 'username');
+const postedAtLabel = computed(() => date.formatXPreview(props.postedAt));
const { card: linkCard, loading: linkCardLoading } = useLinkCard(
toRef(props, 'content'),
@@ -127,7 +130,7 @@ const { card: linkCard, loading: linkCardLoading } = useLinkCard(
- 4:21 PM · Jan 20, 2026
+ {{ postedAtLabel }}
·
3.5M
Views
diff --git a/resources/js/components/ui/date-range-picker/DateRangePicker.vue b/resources/js/components/ui/date-range-picker/DateRangePicker.vue
index 65b223b71..e067cfe84 100644
--- a/resources/js/components/ui/date-range-picker/DateRangePicker.vue
+++ b/resources/js/components/ui/date-range-picker/DateRangePicker.vue
@@ -16,6 +16,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"
import { RangeCalendar } from "@/components/ui/range-calendar"
+import { useCalendarLocale } from "@/composables/useCalendarLocale"
import { cn } from "@/lib/utils"
import dayjs from "@/dayjs"
@@ -28,6 +29,8 @@ const emit = defineEmits<{
'update:modelValue': [value: { start: Date, end: Date }]
}>()
+const calendarLocale = useCalendarLocale()
+
const toCalendarDate = (dateValue: Date) => {
return new CalendarDate(
dateValue.getFullYear(),
@@ -127,11 +130,11 @@ watch(
>
- {{ dayjs(toDate(value.start)).format('D MMM YYYY') }} -
- {{ dayjs(toDate(value.end)).format('D MMM YYYY') }}
+ {{ dayjs(toDate(value.start)).format('LL') }} -
+ {{ dayjs(toDate(value.end)).format('LL') }}
- {{ dayjs(toDate(value.start)).format('D MMM YYYY') }}
+ {{ dayjs(toDate(value.start)).format('LL') }}
@@ -164,6 +167,7 @@ watch(
diff --git a/resources/js/composables/useCalendarLocale.ts b/resources/js/composables/useCalendarLocale.ts
new file mode 100644
index 000000000..2d7f0223a
--- /dev/null
+++ b/resources/js/composables/useCalendarLocale.ts
@@ -0,0 +1,15 @@
+import { usePage } from '@inertiajs/vue3';
+import { computed, type ComputedRef } from 'vue';
+
+/**
+ * Active UI locale for Reka Calendar / RangeCalendar.
+ *
+ * Mirrors the shared Inertia `locale` prop (same codes as config/languages.php
+ * and dayjs), so weekday headers and calendar a11y labels follow the user's
+ * language instead of the Reka English default.
+ */
+export const useCalendarLocale = (): ComputedRef => {
+ const page = usePage();
+
+ return computed(() => (page.props.locale as string | undefined) || 'en');
+};
diff --git a/resources/js/date.ts b/resources/js/date.ts
index cab538ccf..ba72480e0 100644
--- a/resources/js/date.ts
+++ b/resources/js/date.ts
@@ -8,17 +8,125 @@ function getUserTimezone(): string {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
+/** Resolve scheduled local datetime for platform previews, else now. */
+const resolvePreviewPostedAt = (postedAt?: string | null) => {
+ if (postedAt) {
+ const parsed = dayjs(postedAt);
+ if (parsed.isValid()) {
+ return parsed;
+ }
+ }
+
+ return dayjs();
+};
+
+/** X / Bluesky style: `4:21 PM · Aug 5, 2026` (locale-aware). */
+const formatAbsolutePreviewPostedAt = (postedAt?: string | null) => {
+ const instant = resolvePreviewPostedAt(postedAt);
+
+ return `${instant.format('LT')} · ${instant.format('ll')}`;
+};
+
export default {
formatDate(date: string | null | undefined) {
if (!date) return '-';
return dayjs.utc(date).tz(getUserTimezone()).format('LL');
},
- formatDateTime(date: string) {
- return dayjs
- .utc(date)
- .tz(getUserTimezone())
- .format('D [de] MMM [de] YYYY [às] HH:mm');
+ /**
+ * Format a calendar date (expiry, due date, etc.) without shifting the day
+ * across timezones. Use for values stored as UTC midnight that represent a
+ * chosen calendar day rather than an exact instant.
+ */
+ formatDateOnly(date: string | null | undefined) {
+ if (!date) {
+ return '-';
+ }
+
+ return dayjs.utc(date).format('LL');
+ },
+
+ formatDateTime(date: string | null | undefined) {
+ if (!date) {
+ return '—';
+ }
+
+ return dayjs.utc(date).tz(getUserTimezone()).format('LLL');
+ },
+
+ /**
+ * Format a local (already timezone-converted) datetime string.
+ * Use for datetime-local values and other non-UTC inputs.
+ */
+ formatLocalDateTime(date: string | null | undefined) {
+ if (!date) {
+ return '—';
+ }
+
+ return dayjs(date).format('lll');
+ },
+
+ /**
+ * Format a local date-only value (YYYY-MM-DD or Date) with the active locale.
+ */
+ formatLocalDate(date: string | Date | null | undefined) {
+ if (!date) {
+ return '—';
+ }
+
+ return dayjs(date).format('LL');
+ },
+
+ /**
+ * Short day + month for chart axes (day-first so locales keep natural order).
+ */
+ formatMonthDay(date: string | Date) {
+ return dayjs(date).format('D MMM');
+ },
+
+ /**
+ * Short month + day + year for chart tooltips (locale-aware via L).
+ */
+ formatMonthDayYear(date: string | Date) {
+ return dayjs(date).format('L');
+ },
+
+ formatXPreview(postedAt?: string | null) {
+ return formatAbsolutePreviewPostedAt(postedAt);
+ },
+
+ formatBlueskyPreview(postedAt?: string | null) {
+ return formatAbsolutePreviewPostedAt(postedAt);
+ },
+
+ formatMastodonPreview(postedAt?: string | null) {
+ return resolvePreviewPostedAt(postedAt).format('lll');
+ },
+
+ /**
+ * @param justNowLabel Localized fallback when no schedule is set (e.g. common.just_now).
+ */
+ formatFacebookPreview(postedAt?: string | null, justNowLabel?: string) {
+ if (! postedAt && justNowLabel) {
+ return justNowLabel;
+ }
+
+ return resolvePreviewPostedAt(postedAt).fromNow();
+ },
+
+ /**
+ * @param todayLabel Localized same-day prefix (e.g. common.date_range_picker.today).
+ */
+ formatDiscordPreview(postedAt?: string | null, todayLabel?: string) {
+ const instant = resolvePreviewPostedAt(postedAt);
+
+ if (instant.isSame(dayjs(), 'day')) {
+ return todayLabel
+ ? `${todayLabel} · ${instant.format('LT')}`
+ : instant.format('LT');
+ }
+
+ return instant.format('lll');
},
formatTime(date: string | null | undefined) {
@@ -112,7 +220,7 @@ export default {
* @returns String formatada (ex: "Fev/2025")
*/
formatMonthYear(month: number, year: number): string {
- return dayjs(new Date(year, month - 1, 1)).format('MMM/YYYY');
+ return dayjs(new Date(year, month - 1, 1)).format('MMM YYYY');
},
formatAge(birthDate: string): string {
@@ -145,7 +253,7 @@ export default {
* @returns String formatada (ex: "31/12/2025 14:25")
*/
formatBuildDate(date: string): string {
- return dayjs.utc(date).tz(getUserTimezone()).format('DD/MM/YYYY HH:mm');
+ return dayjs.utc(date).tz(getUserTimezone()).format('L LT');
},
/**
diff --git a/resources/js/pages/automations/Index.vue b/resources/js/pages/automations/Index.vue
index 9b7f2bb84..135ed42e5 100644
--- a/resources/js/pages/automations/Index.vue
+++ b/resources/js/pages/automations/Index.vue
@@ -17,7 +17,7 @@ import {
TableLoadMore,
TableRow,
} from '@/components/ui/table';
-import dayjs from '@/dayjs';
+import date from '@/date';
import AppLayout from '@/layouts/AppLayout.vue';
import {
metrics as metricsAutomation,
@@ -46,7 +46,7 @@ const statusConfig = (status: string) => {
return configs[status] ?? configs['draft'];
};
-const formatDate = (date: string) => dayjs.utc(date).local().format('D MMM YYYY');
+const formatDate = (value: string) => date.formatDate(value);
const isCreating = ref(false);
diff --git a/resources/js/pages/labels/Index.vue b/resources/js/pages/labels/Index.vue
index f5c1e7f74..28cc25aac 100644
--- a/resources/js/pages/labels/Index.vue
+++ b/resources/js/pages/labels/Index.vue
@@ -20,7 +20,7 @@ import {
TableLoadMore,
TableRow,
} from '@/components/ui/table';
-import dayjs from '@/dayjs';
+import date from '@/date';
import debounce from '@/debounce';
import AppLayout from '@/layouts/AppLayout.vue';
import { destroy as labelsDestroy, index as labelsIndex } from '@/routes/app/labels';
@@ -73,7 +73,7 @@ const handleDelete = (label: Label) => {
});
};
-const formatDate = (date: string): string => dayjs.utc(date).local().format('D MMM YYYY');
+const formatDate = (value: string): string => date.formatDate(value);
const hasActiveSearch = computed(() => Boolean(searchQuery.value?.trim()));
diff --git a/resources/js/pages/posts/Calendar.vue b/resources/js/pages/posts/Calendar.vue
index 218b201ea..dfda42f4a 100644
--- a/resources/js/pages/posts/Calendar.vue
+++ b/resources/js/pages/posts/Calendar.vue
@@ -87,12 +87,12 @@ const weekdayNames = computed(() => {
return names;
});
+const formatDayMonth = (day: dayjs.Dayjs): string => day.format('D MMMM');
+
// Day view computed
const currentDay = computed(() => dayjs(props.currentDay));
-const dayHeaderTitle = computed(() => {
- return currentDay.value.format('dddd, D [de] MMMM [de] YYYY');
-});
+const dayHeaderTitle = computed(() => currentDay.value.format('LL'));
const dayPosts = computed(() => {
const dateKey = currentDay.value.format('YYYY-MM-DD');
@@ -117,18 +117,22 @@ const weekHeaderTitle = computed(() => {
const start = weekStart.value;
const end = weekStart.value.add(6, 'day');
- if (start.month() === end.month()) {
- return `${start.format('MMMM D')} - ${end.format('D, YYYY')}`;
+ // Day-first tokens so locales like pt-BR stay "3–9 de agosto", not "August 3–9".
+ if (start.isSame(end, 'month')) {
+ return `${start.format('D')}–${end.format('D MMMM YYYY')}`;
+ }
+
+ if (start.isSame(end, 'year')) {
+ return `${start.format('D MMM')} – ${end.format('D MMMM YYYY')}`;
}
- return `${start.format('MMM D')} - ${end.format('MMM D, YYYY')}`;
+
+ return `${start.format('ll')} – ${end.format('ll')}`;
});
// Month view computed
const monthDate = computed(() => dayjs(props.currentMonth));
-const monthHeaderTitle = computed(() => {
- return monthDate.value.format('MMMM YYYY');
-});
+const monthHeaderTitle = computed(() => monthDate.value.format('MMMM YYYY'));
const calendarDays = computed(() => {
const start = monthDate.value.startOf('month').startOf('week');
@@ -392,7 +396,7 @@ const formatTime = (scheduledAt: string): string => {
class="mt-1 text-sm font-bold capitalize"
:class="isToday(day) ? 'text-foreground' : 'text-foreground/80'"
>
- {{ day.format('D/MMMM') }}
+ {{ formatDayMonth(day) }}