Skip to content
Closed
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
16 changes: 13 additions & 3 deletions packages/common/src/i18n/format_date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ function getDateTranslation(

/**
* Returns a date formatter that transforms a date and an offset into a timezone with ISO8601 or
* GMT format depending on the width (eg: short = +0430, short:GMT = GMT+4, long = GMT+04:30,
* GMT format depending on the width (eg: short = +0430, short:GMT = GMT+4:30, long = GMT+04:30,
* extended = +04:30)
*/
function timeZoneGetter(width: ZoneWidth): DateFormatter {
Expand All @@ -473,8 +473,18 @@ function timeZoneGetter(width: ZoneWidth): DateFormatter {
padNumber(hours, 2, minusSign) +
padNumber(Math.abs(zone % 60), 2, minusSign)
);
case ZoneWidth.ShortGMT:
return 'GMT' + (zone >= 0 ? '+' : '') + padNumber(hours, 1, minusSign);
case ZoneWidth.ShortGMT: {
// The short localized GMT format omits a leading zero on the hours but,
// per CLDR, still includes the minutes when they are non-zero (e.g.
// `GMT+5:30` for India, `GMT-4` for New York).
const minutes = Math.abs(zone % 60);
return (
'GMT' +
(zone >= 0 ? '+' : '') +
padNumber(hours, 1, minusSign) +
(minutes ? ':' + padNumber(minutes, 2, minusSign) : '')
);
}
Comment on lines +476 to +487

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that true for every locale ? (the CLDR is a funny rabbithole)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, it is a bit of a rabbit hole indeed. No, the full format isn't identical across locales. I checked Intl short offsets for a +5:30 zone:

  • most locales (en, de, ja, hi, tr, …): GMT+5:30
  • fr: UTC+5:30 (UTC, not GMT)
  • fi: UTC+5.30 (UTC and a . separator)
  • ar: غرينتش+5:30 (translated label)
  • fa: ‎+۵:۳۰ گرینویچ (Persian digits, label after the offset, RTL)

So the literal, separator, digits and order are all locale-dependent. What is universal is that the minutes are kept when they're non-zero, and that's the only thing this change touches.

timeZoneGetter already renders a locale-independent GMT±H[:mm] for every locale (same as the Long/Extended widths), so this doesn't add locale divergence, it just stops the short form dropping the minutes the long form already keeps.

I can reword the code comment to call that out if you think it's worth it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is more : This is not worth fixing and users should rely on Intl if they want top-tier date i18n.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense, agreed that Intl is the right call for proper date i18n. I'll close this out. Thanks for the look, I appreciate it.

case ZoneWidth.Long:
return (
'GMT' +
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/pipes/date_pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const DATE_PIPE_DEFAULT_OPTIONS = new InjectionToken<DatePipeConfig>(
* | Zone | z, zz & zzz | Short specific non location format (fallback to O) | GMT-8 |
* | | zzzz | Long specific non location format (fallback to OOOO) | GMT-08:00 |
* | | Z, ZZ & ZZZ | ISO8601 basic format | -0800 |
* | | ZZZZ | Long localized GMT format | GMT-8:00 |
* | | ZZZZ | Long localized GMT format | GMT-08:00 |
* | | ZZZZZ | ISO8601 extended format + Z indicator for offset 0 (= XXXXX) | -08:00 |
* | | O, OO & OOO | Short localized GMT format | GMT-8 |
* | | OOOO | Long localized GMT format | GMT-08:00 |
Expand Down
29 changes: 20 additions & 9 deletions packages/common/test/i18n/format_date_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,24 +287,28 @@ describe('Format date', () => {

it('should format with timezones', () => {
const dateFixtures: any = {
z: /GMT(\+|-)\d/,
zz: /GMT(\+|-)\d/,
zzz: /GMT(\+|-)\d/,
z: /GMT(\+|-)\d\:30/,
zz: /GMT(\+|-)\d\:30/,
zzz: /GMT(\+|-)\d\:30/,
zzzz: /GMT(\+|-)\d{2}\:30/,
Z: /(\+|-)\d{2}30/,
ZZ: /(\+|-)\d{2}30/,
ZZZ: /(\+|-)\d{2}30/,
ZZZZ: /GMT(\+|-)\d{2}\:30/,
ZZZZZ: /(\+|-)\d{2}\:30/,
O: /GMT(\+|-)\d/,
O: /GMT(\+|-)\d\:30/,
OOOO: /GMT(\+|-)\d{2}\:30/,
};

Object.keys(dateFixtures).forEach((pattern: string) => {
expect(formatDate(date, pattern, ɵDEFAULT_LOCALE_ID, '+0430')).toMatch(
dateFixtures[pattern],
);
});
// Both a positive and a negative fractional offset, so the short GMT
// format is exercised for each sign with non-zero minutes.
for (const offset of ['+0430', '-0330']) {
Object.keys(dateFixtures).forEach((pattern: string) => {
expect(formatDate(date, pattern, ɵDEFAULT_LOCALE_ID, offset)).toMatch(
dateFixtures[pattern],
);
});
}
});

it('should format common multi component patterns', () => {
Expand Down Expand Up @@ -528,6 +532,13 @@ describe('Format date', () => {
const dateEst = formatDate(isoDate, 'long', 'en', 'EST');
expect(dateEst).toBe('February 17, 2024, 7:00:00 AM GMT-5');

// Short GMT (`O`) keeps non-zero minutes for fractional zones, drops them
// for whole-hour zones, for both signs.
expect(formatDate(isoDate, 'O', 'en', '+0530')).toBe('GMT+5:30');
expect(formatDate(isoDate, 'O', 'en', '+0545')).toBe('GMT+5:45');
expect(formatDate(isoDate, 'O', 'en', '-0330')).toBe('GMT-3:30');
expect(formatDate(isoDate, 'O', 'en', '-0500')).toBe('GMT-5');

const dateOffset = formatDate(isoDate, 'long', 'en', '+0500');
expect(dateOffset).toBe('February 17, 2024, 5:00:00 PM GMT+5');
});
Expand Down
Loading