Skip to content

Commit

Permalink
feat: support short/default/long/full date time formats
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko committed Dec 21, 2024
1 parent e45a2af commit abd2e51
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 71 deletions.
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const tsConfigPathMapping = pathsToModuleNameMapper(
)

const testMatch = ["**/?(*.)test.(js|ts|tsx)", "**/test/index.(js|ts|tsx)"]

const transformIgnorePatterns = ["node_modules/(?!@messageformat)"]
/**
* @type {import('jest').Config}
*/
Expand Down Expand Up @@ -38,13 +38,15 @@ module.exports = {
displayName: "web",
testEnvironment: "jsdom",
testMatch,
transformIgnorePatterns,
moduleNameMapper: tsConfigPathMapping,
roots: ["<rootDir>/packages/react"],
},
{
displayName: "universal",
testEnvironment: "jest-environment-node-single-context",
testMatch,
transformIgnorePatterns,
moduleNameMapper: tsConfigPathMapping,
roots: ["<rootDir>/packages/core", "<rootDir>/packages/remote-loader"],
},
Expand All @@ -57,6 +59,7 @@ module.exports = {
require.resolve("./scripts/jest/stripAnsiSerializer.js"),
],
setupFilesAfterEnv: [require.resolve("./scripts/jest/env.js")],
transformIgnorePatterns,
roots: [
"<rootDir>/packages/babel-plugin-extract-messages",
"<rootDir>/packages/babel-plugin-lingui-macro",
Expand Down
29 changes: 1 addition & 28 deletions packages/core/src/formats.test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,6 @@
import { date, number } from "./formats"
import { number } from "./formats"

describe("@lingui/core/formats", () => {
describe("date", () => {
it("should support Date as input", () => {
expect(date(["en"], new Date(2023, 2, 5))).toBe("3/5/2023")
})
it("should support iso string as input", () => {
expect(date(["en"], new Date(2023, 2, 5).toISOString())).toBe("3/5/2023")
})

it("should pass format options", () => {
expect(
date(["en"], new Date(2023, 2, 5).toISOString(), { dateStyle: "full" })
).toBe("Sunday, March 5, 2023")

expect(
date(["en"], new Date(2023, 2, 5).toISOString(), {
dateStyle: "medium",
})
).toBe("Mar 5, 2023")
})

it("should respect passed locale", () => {
expect(
date(["pl"], new Date(2023, 2, 5).toISOString(), { dateStyle: "full" })
).toBe("niedziela, 5 marca 2023")
})
})

describe("number", () => {
it("should pass format options", () => {
expect(number(["en"], 1000, { style: "currency", currency: "EUR" })).toBe(
Expand Down
77 changes: 70 additions & 7 deletions packages/core/src/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,88 @@ function normalizeLocales(locales: Locales): string[] {
return [...out, defaultLocale]
}

export type DateTimeFormatSize = "short" | "default" | "long" | "full"

export function date(
locales: Locales,
value: string | Date,
format?: Intl.DateTimeFormatOptions
format?: Intl.DateTimeFormatOptions | DateTimeFormatSize
): string {
const _locales = normalizeLocales(locales)

if (!format) {
format = "default"
}

let o: Intl.DateTimeFormatOptions

if (typeof format === "string") {
// Implementation is taken from
// https://github.com/messageformat/messageformat/blob/df2da92bf6541a77aac2ce3cdcd0100bed2b2c5b/mf1/packages/runtime/src/fmt/date.ts
o = {
day: "numeric",
month: "short",
year: "numeric",
}

/* eslint-disable no-fallthrough */
switch (format) {
case "full":
o.weekday = "long"
case "long":
o.month = "long"
break
case "short":
o.month = "numeric"
break
}
} else {
o = format
}

const formatter = getMemoized(
() => cacheKey("date", _locales, format),
() => new Intl.DateTimeFormat(_locales, format)
() => new Intl.DateTimeFormat(_locales, o)
)

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

export function time(
locales: Locales,
value: string | Date,
format?: Intl.DateTimeFormatOptions | DateTimeFormatSize
): string {
let o: Intl.DateTimeFormatOptions

if (!format) {
format = "default"
}

if (typeof format === "string") {
// https://github.com/messageformat/messageformat/blob/df2da92bf6541a77aac2ce3cdcd0100bed2b2c5b/mf1/packages/runtime/src/fmt/time.ts

o = {
second: "numeric",
minute: "numeric",
hour: "numeric",
}

switch (format) {
case "full":
case "long":
o.timeZoneName = "short"
break
case "short":
delete o.second
}
} else {
o = format
}

return date(locales, value, o)
}

export function number(
locales: Locales,
value: number,
Expand Down Expand Up @@ -78,11 +145,7 @@ function getMemoized<T>(getKey: () => string, construct: () => T) {
return formatter
}

function cacheKey(
type: string,
locales: readonly string[],
options?: Intl.DateTimeFormatOptions | Intl.NumberFormatOptions
) {
function cacheKey(type: string, locales: readonly string[], options?: unknown) {
const localeKey = locales.join("-")
return `${type}-${localeKey}-${JSON.stringify(options)}`
}
187 changes: 187 additions & 0 deletions packages/core/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,191 @@ describe("I18n", () => {
expect(i18n._("Software development")).toEqual("Software­entwicklung")
expect(i18n._("Software development")).toEqual("Software­entwicklung")
})

describe("ICU date format", () => {
const i18n = setupI18n({
locale: "fr",
messages: { fr: {} },
})

const date = new Date("2014-12-06")

it("style short", () => {
expect(
i18n._("It starts on {someDate, date, short}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 06/12/2014"`)
})

it("style full", () => {
expect(
i18n._("It starts on {someDate, date, full}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on samedi 6 décembre 2014"`)
})

it("style long", () => {
expect(
i18n._("It starts on {someDate, date, long}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 6 décembre 2014"`)
})

it("style default", () => {
expect(
i18n._("It starts on {someDate, date, default}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 6 déc. 2014"`)
})

it("no style", () => {
expect(
i18n._("It starts on {someDate, date}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 6 déc. 2014"`)
})

it("using custom style", () => {
expect(
i18n._(
"It starts on {someDate, date, myStyle}",
{
someDate: date,
},
{
formats: {
myStyle: {
day: "numeric",
},
},
}
)
).toMatchInlineSnapshot(`"It starts on 6"`)
})

it("using date skeleton", () => {
expect(
i18n._("It starts on {someDate, date, ::GrMMMdd}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 06 déc. 2014 ap. J.-C."`)
})

it("should respect locale", () => {
const i18n = setupI18n({
locale: "fr",
messages: { fr: {}, pl: {} },
})

const msg = "It starts on {someDate, date, long}"

expect(
i18n._(msg, {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 6 décembre 2014"`)

i18n.activate("pl")

expect(
i18n._(msg, {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 6 grudnia 2014"`)
})
})
describe("ICU time format", () => {
const i18n = setupI18n({
locale: "fr",
messages: { fr: {} },
})

const date = new Date("2014-12-06::17:40 UTC")

it("style short", () => {
expect(
i18n._("It starts on {someDate, time, short}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40"`)
})

it("style full", () => {
expect(
i18n._("It starts on {someDate, time, full}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)
})

it("style long", () => {
expect(
i18n._("It starts on {someDate, time, long}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)
})

it("style default", () => {
expect(
i18n._("It starts on {someDate, time, default}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40:00"`)
})

it("no style", () => {
expect(
i18n._("It starts on {someDate, time}", {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40:00"`)
})

it("using custom style", () => {
expect(
i18n._(
"It starts on {someDate, time, myStyle}",
{
someDate: date,
},
{
formats: {
myStyle: {
hour: "numeric",
},
},
}
)
).toMatchInlineSnapshot(`"It starts on 17 h"`)
})

it("should respect locale", () => {
const i18n = setupI18n({
locale: "fr",
messages: { fr: {}, "en-US": {} },
})

const msg = "It starts on {someDate, time, long}"

expect(
i18n._(msg, {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 17:40:00 UTC"`)

i18n.activate("en-US")

expect(
i18n._(msg, {
someDate: date,
})
).toMatchInlineSnapshot(`"It starts on 5:40:00 PM UTC"`)
})
})
})
Loading

0 comments on commit abd2e51

Please sign in to comment.