From a24be805b38cbee2f4d9444f7e7fff628c42d0e6 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Wed, 2 Apr 2025 11:23:17 +0200 Subject: [PATCH 1/5] Override valueOf in FluentDateTime so that it can be converted to a number implicitely --- fluent-bundle/src/builtins.ts | 2 +- fluent-bundle/src/types.ts | 2 +- fluent-bundle/test/temporal_test.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fluent-bundle/src/builtins.ts b/fluent-bundle/src/builtins.ts index 2862616c..8b99a6ba 100644 --- a/fluent-bundle/src/builtins.ts +++ b/fluent-bundle/src/builtins.ts @@ -88,7 +88,7 @@ export function NUMBER( } if (arg instanceof FluentDateTime) { - return new FluentNumber(arg.toNumber(), { + return new FluentNumber(arg.valueOf(), { ...values(opts, NUMBER_ALLOWED), }); } diff --git a/fluent-bundle/src/types.ts b/fluent-bundle/src/types.ts index a78fa9c1..cb1d5415 100644 --- a/fluent-bundle/src/types.ts +++ b/fluent-bundle/src/types.ts @@ -195,7 +195,7 @@ export class FluentDateTime extends FluentType { * Note that this isn't always possible due to the nature of Temporal objects. * In such cases, a TypeError will be thrown. */ - toNumber(): number { + valueOf(): number { const value = this.value; if (typeof value === "number") return value; if (value instanceof Date) return value.getTime(); diff --git a/fluent-bundle/test/temporal_test.js b/fluent-bundle/test/temporal_test.js index 6219b617..2af66a66 100644 --- a/fluent-bundle/test/temporal_test.js +++ b/fluent-bundle/test/temporal_test.js @@ -66,7 +66,7 @@ suite("Temporal support", function () { test("can be converted to a number", function () { arg = new FluentDateTime(arg); - assert.strictEqual(arg.toNumber(), 0); + assert.strictEqual(+arg, 0); }); }); @@ -94,7 +94,7 @@ suite("Temporal support", function () { test("can be converted to a number", function () { arg = new FluentDateTime(arg); - assert.strictEqual(arg.toNumber(), 0); + assert.strictEqual(+arg, 0); }); }); @@ -123,7 +123,7 @@ suite("Temporal support", function () { test("can be converted to a number", function () { arg = new FluentDateTime(arg); - assert.strictEqual(arg.toNumber(), 0); + assert.strictEqual(+arg, 0); }); }); } @@ -171,7 +171,7 @@ suite("Temporal support", function () { test("cannot be converted to a number", function () { arg = new FluentDateTime(arg); - assert.throws(() => arg.toNumber(), TypeError); + assert.throws(() => +arg, TypeError); }); }); @@ -203,7 +203,7 @@ suite("Temporal support", function () { test("cannot be converted to a number", function () { arg = new FluentDateTime(arg); - assert.throws(() => arg.toNumber(), TypeError); + assert.throws(() => +arg, TypeError); }); }); }); From b6f053534edbcc84d570f0b4246c707c83b76d99 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 2 Apr 2025 13:20:08 +0300 Subject: [PATCH 2/5] Revert renaming FluentDateTime's toNumber() as valueOf() --- fluent-bundle/src/builtins.ts | 2 +- fluent-bundle/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fluent-bundle/src/builtins.ts b/fluent-bundle/src/builtins.ts index 8b99a6ba..2862616c 100644 --- a/fluent-bundle/src/builtins.ts +++ b/fluent-bundle/src/builtins.ts @@ -88,7 +88,7 @@ export function NUMBER( } if (arg instanceof FluentDateTime) { - return new FluentNumber(arg.valueOf(), { + return new FluentNumber(arg.toNumber(), { ...values(opts, NUMBER_ALLOWED), }); } diff --git a/fluent-bundle/src/types.ts b/fluent-bundle/src/types.ts index cb1d5415..a78fa9c1 100644 --- a/fluent-bundle/src/types.ts +++ b/fluent-bundle/src/types.ts @@ -195,7 +195,7 @@ export class FluentDateTime extends FluentType { * Note that this isn't always possible due to the nature of Temporal objects. * In such cases, a TypeError will be thrown. */ - valueOf(): number { + toNumber(): number { const value = this.value; if (typeof value === "number") return value; if (value instanceof Date) return value.getTime(); From 58faf6cd1970225620823b228bec843505905a61 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 2 Apr 2025 13:00:44 +0300 Subject: [PATCH 3/5] Do not require scope argument in FluentNumber & FluentDateTime toString() calls --- fluent-bundle/src/types.ts | 40 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/fluent-bundle/src/types.ts b/fluent-bundle/src/types.ts index a78fa9c1..1ba37635 100644 --- a/fluent-bundle/src/types.ts +++ b/fluent-bundle/src/types.ts @@ -108,14 +108,16 @@ export class FluentNumber extends FluentType { /** * Format this `FluentNumber` to a string. */ - toString(scope: Scope): string { - try { - const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); - return nf.format(this.value); - } catch (err) { - scope.reportError(err); - return this.value.toString(10); + toString(scope?: Scope): string { + if (scope) { + try { + const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); + return nf.format(this.value); + } catch (err) { + scope.reportError(err); + } } + return this.value.toString(10); } } @@ -214,18 +216,20 @@ export class FluentDateTime extends FluentType { /** * Format this `FluentDateTime` to a string. */ - toString(scope: Scope): string { - try { - const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts); - return dtf.format( - this.value as Parameters[0] - ); - } catch (err) { - scope.reportError(err); - if (typeof this.value === "number" || this.value instanceof Date) { - return new Date(this.value).toISOString(); + toString(scope?: Scope): string { + if (scope) { + try { + const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts); + return dtf.format( + this.value as Parameters[0] + ); + } catch (err) { + scope.reportError(err); } - return this.value.toString(); } + if (typeof this.value === "number" || this.value instanceof Date) { + return new Date(this.value).toISOString(); + } + return this.value.toString(); } } From bb40eab005fe4134238d26ef97c6f4a2b0b7b658 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 2 Apr 2025 13:16:25 +0300 Subject: [PATCH 4/5] Add FluentDateTime.p.[Symbol.toPrimitive]() --- fluent-bundle/src/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fluent-bundle/src/types.ts b/fluent-bundle/src/types.ts index 1ba37635..78fb49b5 100644 --- a/fluent-bundle/src/types.ts +++ b/fluent-bundle/src/types.ts @@ -192,6 +192,10 @@ export class FluentDateTime extends FluentType { this.opts = opts; } + [Symbol.toPrimitive](hint: "number" | "string" | "default"): string | number { + return hint === "string" ? this.toString() : this.toNumber(); + } + /** * Convert this `FluentDateTime` to a number. * Note that this isn't always possible due to the nature of Temporal objects. From 096f08e84c95d16810febb6cb4b0f3d9f0374a93 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 2 Apr 2025 13:17:14 +0300 Subject: [PATCH 5/5] Add test suite using code from firefox-devtools/profiler@9c8fb55 --- fluent-bundle/test/functions_runtime_test.js | 91 +++++++++++++++++--- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/fluent-bundle/test/functions_runtime_test.js b/fluent-bundle/test/functions_runtime_test.js index 7ed960e4..ddcc26a3 100644 --- a/fluent-bundle/test/functions_runtime_test.js +++ b/fluent-bundle/test/functions_runtime_test.js @@ -1,18 +1,17 @@ -import assert from "assert"; import ftl from "@fluent/dedent"; +import assert from "assert"; -import { FluentBundle } from "../esm/bundle.js"; -import { FluentResource } from "../esm/resource.js"; -import { FluentNumber } from "../esm/types.js"; +import { + FluentBundle, + FluentDateTime, + FluentNumber, + FluentResource, +} from "../esm/index.js"; suite("Runtime-specific functions", function () { - let bundle, errs; - - setup(function () { - errs = []; - }); - suite("passing into the constructor", function () { + let bundle, errs; + suiteSetup(function () { bundle = new FluentBundle("en-US", { useIsolating: false, @@ -33,6 +32,7 @@ suite("Runtime-specific functions", function () { } `) ); + errs = []; }); test("works for strings", function () { @@ -56,4 +56,75 @@ suite("Runtime-specific functions", function () { assert.strictEqual(errs.length, 0); }); }); + + suite("firefox-devtools/profiler@9c8fb55", () => { + /** @type {FluentBundle} */ + let bundle; + + suiteSetup(() => { + const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; + const ONE_YEAR_IN_MS = 365 * ONE_DAY_IN_MS; + + const DATE_FORMATS = { + thisDay: { hour: "numeric", minute: "numeric" }, + thisYear: { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }, + ancient: { + year: "numeric", + month: "short", + day: "numeric", + }, + }; + + const SHORTDATE = args => { + const date = args[0]; + const nowTimestamp = Number(new Date("2025-02-15T12:00")); + + const timeDifference = nowTimestamp - +date; + if (timeDifference < 0 || timeDifference > ONE_YEAR_IN_MS) { + return new FluentDateTime(date, DATE_FORMATS.ancient); + } + if (timeDifference > ONE_DAY_IN_MS) { + return new FluentDateTime(date, DATE_FORMATS.thisYear); + } + return new FluentDateTime(date, DATE_FORMATS.thisDay); + }; + + const messages = ftl`\nkey = { SHORTDATE($date) }\n`; + const resource = new FluentResource(messages); + bundle = new FluentBundle("en-US", { functions: { SHORTDATE } }); + bundle.addResource(resource); + }); + + test("works with difference in hours", function () { + const msg = bundle.getMessage("key"); + const date = new Date("2025-02-15T10:30"); + const errs = []; + const val = bundle.formatPattern(msg.value, { date }, errs); + assert.strictEqual(val, "10:30 AM"); + assert.strictEqual(errs.length, 0); + }); + + test("works with difference in days", function () { + const msg = bundle.getMessage("key"); + const date = new Date("2025-02-03T10:30"); + const errs = []; + const val = bundle.formatPattern(msg.value, { date }, errs); + assert.strictEqual(val, "Feb 3, 10:30 AM"); + assert.strictEqual(errs.length, 0); + }); + + test("works with difference in years", function () { + const msg = bundle.getMessage("key"); + const date = new Date("2023-02-03T10:30"); + const errs = []; + const val = bundle.formatPattern(msg.value, { date }, errs); + assert.strictEqual(val, "Feb 3, 2023"); + assert.strictEqual(errs.length, 0); + }); + }); });