Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/meteor/client/lib/utils/dateFormat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { formatDate, momentFormatToDateFns } from './dateFormat';

describe('momentFormatToDateFns', () => {
it('maps locale tokens', () => {
expect(momentFormatToDateFns('L')).toBe('P');
expect(momentFormatToDateFns('LT')).toBe('p');
expect(momentFormatToDateFns('LTS')).toBe('pp');
expect(momentFormatToDateFns('LL')).toBe('PPP');
expect(momentFormatToDateFns('LLL')).toBe('PPP p');
expect(momentFormatToDateFns('LLLL')).toBe('EEEE, PPP p');
});

it('maps common tokens', () => {
expect(momentFormatToDateFns('YYYY-MM-DD HH:mm:ss')).toBe('yyyy-MM-dd HH:mm:ss');
expect(momentFormatToDateFns('MMMM Do YYYY, h:mm:ss a')).toBe('MMMM do yyyy, h:mm:ss a');
});

it('translates moment [literal] escape to date-fns single-quoted literal', () => {
expect(momentFormatToDateFns('[Today at] LT')).toBe("'Today at' p");
expect(momentFormatToDateFns('[Session started at] HH:mm [on] LL')).toBe("'Session started at' HH:mm 'on' PPP");
});

it("escapes embedded single quotes inside literals as ''", () => {
expect(momentFormatToDateFns("[it's] LT")).toBe("'it''s' p");
});

it('preserves empty literal blocks', () => {
expect(momentFormatToDateFns('[] LT')).toBe("'' p");
});
});

describe('formatDate', () => {
const sample = new Date('2026-04-24T20:30:45');

it('formats literal blocks with locale tokens without throwing', () => {
expect(() => formatDate(sample, '[Today at] LT')).not.toThrow();
expect(formatDate(sample, '[Today at] LT')).toMatch(/^Today at /);
});

it('formats date with year-month-day token string', () => {
expect(formatDate(sample, 'YYYY-MM-DD')).toBe('2026-04-24');
});
});
24 changes: 19 additions & 5 deletions apps/meteor/client/lib/utils/dateFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type DateInput = string | Date | number;
/**
* Map moment-style locale format tokens to date-fns format string.
* Used for Message_DateFormat and Message_TimeFormat settings (defaults: LL, LT).
* Moment's `[literal]` escape syntax is translated to date-fns' `'literal'` syntax.
*/
export const momentFormatToDateFns = (momentFormat: string): string => {
const tokenMap: Record<string, string> = {
Expand Down Expand Up @@ -39,12 +40,25 @@ export const momentFormatToDateFns = (momentFormat: string): string => {
A: 'a',
a: 'a',
};
let out = momentFormat;
const entries = Object.entries(tokenMap).sort(([a], [b]) => b.length - a.length);
for (const [mom, df] of entries) {
out = out.replace(new RegExp(mom.replace(/([.*+?^${}()|[\]\\])/g, '\\$1'), 'g'), df);
}
return out;

const replaceTokens = (input: string): string => {
let out = input;
for (const [mom, df] of entries) {
out = out.replace(new RegExp(mom.replace(/([.*+?^${}()|[\]\\])/g, '\\$1'), 'g'), df);
}
return out;
};

return momentFormat
.split(/(\[[^\]]*\])/g)
.map((part) => {
if (part.startsWith('[') && part.endsWith(']')) {
return `'${part.slice(1, -1).replace(/'/g, "''")}'`;
}
return replaceTokens(part);
})
.join('');
};

export const formatDate = (date: DateInput, formatStr: string, locale?: Locale): string => {
Expand Down
Loading