Skip to content

Commit 8020427

Browse files
feat: support short/default/long/full date time formats (#2117)
1 parent 2026c83 commit 8020427

11 files changed

+391
-71
lines changed

jest.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const tsConfigPathMapping = pathsToModuleNameMapper(
99
)
1010

1111
const testMatch = ["**/?(*.)test.(js|ts|tsx)", "**/test/index.(js|ts|tsx)"]
12-
12+
const transformIgnorePatterns = ["node_modules/(?!@messageformat)"]
1313
/**
1414
* @type {import('jest').Config}
1515
*/
@@ -38,13 +38,15 @@ module.exports = {
3838
displayName: "web",
3939
testEnvironment: "jsdom",
4040
testMatch,
41+
transformIgnorePatterns,
4142
moduleNameMapper: tsConfigPathMapping,
4243
roots: ["<rootDir>/packages/react"],
4344
},
4445
{
4546
displayName: "universal",
4647
testEnvironment: "jest-environment-node-single-context",
4748
testMatch,
49+
transformIgnorePatterns,
4850
moduleNameMapper: tsConfigPathMapping,
4951
roots: ["<rootDir>/packages/core", "<rootDir>/packages/remote-loader"],
5052
},
@@ -57,6 +59,7 @@ module.exports = {
5759
require.resolve("./scripts/jest/stripAnsiSerializer.js"),
5860
],
5961
setupFilesAfterEnv: [require.resolve("./scripts/jest/env.js")],
62+
transformIgnorePatterns,
6063
roots: [
6164
"<rootDir>/packages/babel-plugin-extract-messages",
6265
"<rootDir>/packages/babel-plugin-lingui-macro",

packages/core/src/formats.test.ts

+1-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,6 @@
1-
import { date, number } from "./formats"
1+
import { number } from "./formats"
22

33
describe("@lingui/core/formats", () => {
4-
describe("date", () => {
5-
it("should support Date as input", () => {
6-
expect(date(["en"], new Date(2023, 2, 5))).toBe("3/5/2023")
7-
})
8-
it("should support iso string as input", () => {
9-
expect(date(["en"], new Date(2023, 2, 5).toISOString())).toBe("3/5/2023")
10-
})
11-
12-
it("should pass format options", () => {
13-
expect(
14-
date(["en"], new Date(2023, 2, 5).toISOString(), { dateStyle: "full" })
15-
).toBe("Sunday, March 5, 2023")
16-
17-
expect(
18-
date(["en"], new Date(2023, 2, 5).toISOString(), {
19-
dateStyle: "medium",
20-
})
21-
).toBe("Mar 5, 2023")
22-
})
23-
24-
it("should respect passed locale", () => {
25-
expect(
26-
date(["pl"], new Date(2023, 2, 5).toISOString(), { dateStyle: "full" })
27-
).toBe("niedziela, 5 marca 2023")
28-
})
29-
})
30-
314
describe("number", () => {
325
it("should pass format options", () => {
336
expect(number(["en"], 1000, { style: "currency", currency: "EUR" })).toBe(

packages/core/src/formats.ts

+70-7
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,88 @@ function normalizeLocales(locales: Locales): string[] {
1111
return [...out, defaultLocale]
1212
}
1313

14+
export type DateTimeFormatSize = "short" | "default" | "long" | "full"
15+
1416
export function date(
1517
locales: Locales,
1618
value: string | Date,
17-
format?: Intl.DateTimeFormatOptions
19+
format?: Intl.DateTimeFormatOptions | DateTimeFormatSize
1820
): string {
1921
const _locales = normalizeLocales(locales)
2022

23+
if (!format) {
24+
format = "default"
25+
}
26+
27+
let o: Intl.DateTimeFormatOptions
28+
29+
if (typeof format === "string") {
30+
// Implementation is taken from
31+
// https://github.com/messageformat/messageformat/blob/df2da92bf6541a77aac2ce3cdcd0100bed2b2c5b/mf1/packages/runtime/src/fmt/date.ts
32+
o = {
33+
day: "numeric",
34+
month: "short",
35+
year: "numeric",
36+
}
37+
38+
/* eslint-disable no-fallthrough */
39+
switch (format) {
40+
case "full":
41+
o.weekday = "long"
42+
case "long":
43+
o.month = "long"
44+
break
45+
case "short":
46+
o.month = "numeric"
47+
break
48+
}
49+
} else {
50+
o = format
51+
}
52+
2153
const formatter = getMemoized(
2254
() => cacheKey("date", _locales, format),
23-
() => new Intl.DateTimeFormat(_locales, format)
55+
() => new Intl.DateTimeFormat(_locales, o)
2456
)
2557

2658
return formatter.format(isString(value) ? new Date(value) : value)
2759
}
2860

61+
export function time(
62+
locales: Locales,
63+
value: string | Date,
64+
format?: Intl.DateTimeFormatOptions | DateTimeFormatSize
65+
): string {
66+
let o: Intl.DateTimeFormatOptions
67+
68+
if (!format) {
69+
format = "default"
70+
}
71+
72+
if (typeof format === "string") {
73+
// https://github.com/messageformat/messageformat/blob/df2da92bf6541a77aac2ce3cdcd0100bed2b2c5b/mf1/packages/runtime/src/fmt/time.ts
74+
75+
o = {
76+
second: "numeric",
77+
minute: "numeric",
78+
hour: "numeric",
79+
}
80+
81+
switch (format) {
82+
case "full":
83+
case "long":
84+
o.timeZoneName = "short"
85+
break
86+
case "short":
87+
delete o.second
88+
}
89+
} else {
90+
o = format
91+
}
92+
93+
return date(locales, value, o)
94+
}
95+
2996
export function number(
3097
locales: Locales,
3198
value: number,
@@ -78,11 +145,7 @@ function getMemoized<T>(getKey: () => string, construct: () => T) {
78145
return formatter
79146
}
80147

81-
function cacheKey(
82-
type: string,
83-
locales: readonly string[],
84-
options?: Intl.DateTimeFormatOptions | Intl.NumberFormatOptions
85-
) {
148+
function cacheKey(type: string, locales: readonly string[], options?: unknown) {
86149
const localeKey = locales.join("-")
87150
return `${type}-${localeKey}-${JSON.stringify(options)}`
88151
}

packages/core/src/i18n.test.ts

+187
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,191 @@ describe("I18n", () => {
444444
This issue may also occur due to a race condition in your initialization logic."
445445
`)
446446
})
447+
448+
describe("ICU date format", () => {
449+
const i18n = setupI18n({
450+
locale: "fr",
451+
messages: { fr: {} },
452+
})
453+
454+
const date = new Date("2014-12-06")
455+
456+
it("style short", () => {
457+
expect(
458+
i18n._("It starts on {someDate, date, short}", {
459+
someDate: date,
460+
})
461+
).toMatchInlineSnapshot(`"It starts on 06/12/2014"`)
462+
})
463+
464+
it("style full", () => {
465+
expect(
466+
i18n._("It starts on {someDate, date, full}", {
467+
someDate: date,
468+
})
469+
).toMatchInlineSnapshot(`"It starts on samedi 6 décembre 2014"`)
470+
})
471+
472+
it("style long", () => {
473+
expect(
474+
i18n._("It starts on {someDate, date, long}", {
475+
someDate: date,
476+
})
477+
).toMatchInlineSnapshot(`"It starts on 6 décembre 2014"`)
478+
})
479+
480+
it("style default", () => {
481+
expect(
482+
i18n._("It starts on {someDate, date, default}", {
483+
someDate: date,
484+
})
485+
).toMatchInlineSnapshot(`"It starts on 6 déc. 2014"`)
486+
})
487+
488+
it("no style", () => {
489+
expect(
490+
i18n._("It starts on {someDate, date}", {
491+
someDate: date,
492+
})
493+
).toMatchInlineSnapshot(`"It starts on 6 déc. 2014"`)
494+
})
495+
496+
it("using custom style", () => {
497+
expect(
498+
i18n._(
499+
"It starts on {someDate, date, myStyle}",
500+
{
501+
someDate: date,
502+
},
503+
{
504+
formats: {
505+
myStyle: {
506+
day: "numeric",
507+
},
508+
},
509+
}
510+
)
511+
).toMatchInlineSnapshot(`"It starts on 6"`)
512+
})
513+
514+
it("using date skeleton", () => {
515+
expect(
516+
i18n._("It starts on {someDate, date, ::GrMMMdd}", {
517+
someDate: date,
518+
})
519+
).toMatchInlineSnapshot(`"It starts on 06 déc. 2014 ap. J.-C."`)
520+
})
521+
522+
it("should respect locale", () => {
523+
const i18n = setupI18n({
524+
locale: "fr",
525+
messages: { fr: {}, pl: {} },
526+
})
527+
528+
const msg = "It starts on {someDate, date, long}"
529+
530+
expect(
531+
i18n._(msg, {
532+
someDate: date,
533+
})
534+
).toMatchInlineSnapshot(`"It starts on 6 décembre 2014"`)
535+
536+
i18n.activate("pl")
537+
538+
expect(
539+
i18n._(msg, {
540+
someDate: date,
541+
})
542+
).toMatchInlineSnapshot(`"It starts on 6 grudnia 2014"`)
543+
})
544+
})
545+
describe("ICU time format", () => {
546+
const i18n = setupI18n({
547+
locale: "fr",
548+
messages: { fr: {} },
549+
})
550+
551+
const date = new Date("2014-12-06::17:40 UTC")
552+
553+
it("style short", () => {
554+
expect(
555+
i18n._("It starts on {someDate, time, short}", {
556+
someDate: date,
557+
})
558+
).toMatchInlineSnapshot(`"It starts on 17:40"`)
559+
})
560+
561+
it("style full", () => {
562+
expect(
563+
i18n._("It starts on {someDate, time, full}", {
564+
someDate: date,
565+
})
566+
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)
567+
})
568+
569+
it("style long", () => {
570+
expect(
571+
i18n._("It starts on {someDate, time, long}", {
572+
someDate: date,
573+
})
574+
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)
575+
})
576+
577+
it("style default", () => {
578+
expect(
579+
i18n._("It starts on {someDate, time, default}", {
580+
someDate: date,
581+
})
582+
).toMatchInlineSnapshot(`"It starts on 17:40:00"`)
583+
})
584+
585+
it("no style", () => {
586+
expect(
587+
i18n._("It starts on {someDate, time}", {
588+
someDate: date,
589+
})
590+
).toMatchInlineSnapshot(`"It starts on 17:40:00"`)
591+
})
592+
593+
it("using custom style", () => {
594+
expect(
595+
i18n._(
596+
"It starts on {someDate, time, myStyle}",
597+
{
598+
someDate: date,
599+
},
600+
{
601+
formats: {
602+
myStyle: {
603+
hour: "numeric",
604+
},
605+
},
606+
}
607+
)
608+
).toMatchInlineSnapshot(`"It starts on 17 h"`)
609+
})
610+
611+
it("should respect locale", () => {
612+
const i18n = setupI18n({
613+
locale: "fr",
614+
messages: { fr: {}, "en-US": {} },
615+
})
616+
617+
const msg = "It starts on {someDate, time, long}"
618+
619+
expect(
620+
i18n._(msg, {
621+
someDate: date,
622+
})
623+
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)
624+
625+
i18n.activate("en-US")
626+
627+
expect(
628+
i18n._(msg, {
629+
someDate: date,
630+
})
631+
).toMatchInlineSnapshot(`"It starts on 5:40:00 PM UTC"`)
632+
})
633+
})
447634
})

0 commit comments

Comments
 (0)