Skip to content

Commit f4c5217

Browse files
committed
Implement support for Temporal objects (except Temporal.Duration).
1 parent 48e2a62 commit f4c5217

File tree

9 files changed

+417
-27
lines changed

9 files changed

+417
-27
lines changed

fluent-bundle/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"npm": ">=7.0.0"
5050
},
5151
"devDependencies": {
52-
"@fluent/dedent": "^0.5.0"
52+
"@fluent/dedent": "^0.5.0",
53+
"temporal-polyfill": "^0.2.5"
5354
}
5455
}

fluent-bundle/src/builtins.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function NUMBER(
8888
}
8989

9090
if (arg instanceof FluentDateTime) {
91-
return new FluentNumber(arg.valueOf(), {
91+
return new FluentNumber(arg.toNumber(), {
9292
...values(opts, NUMBER_ALLOWED),
9393
});
9494
}
@@ -157,17 +157,8 @@ export function DATETIME(
157157
return new FluentNone(`DATETIME(${arg.valueOf()})`);
158158
}
159159

160-
if (arg instanceof FluentDateTime) {
161-
return new FluentDateTime(arg.valueOf(), {
162-
...arg.opts,
163-
...values(opts, DATETIME_ALLOWED),
164-
});
165-
}
166-
167-
if (arg instanceof FluentNumber) {
168-
return new FluentDateTime(arg.valueOf(), {
169-
...values(opts, DATETIME_ALLOWED),
170-
});
160+
if (arg instanceof FluentDateTime || arg instanceof FluentNumber) {
161+
return new FluentDateTime(arg, values(opts, DATETIME_ALLOWED));
171162
}
172163

173164
throw new TypeError("Invalid argument to DATETIME");

fluent-bundle/src/bundle.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { resolveComplexPattern } from "./resolver.js";
22
import { Scope } from "./scope.js";
33
import { FluentResource } from "./resource.js";
4-
import { FluentValue, FluentNone, FluentFunction } from "./types.js";
4+
import { FluentVariable, FluentNone, FluentFunction } from "./types.js";
55
import { Message, Term, Pattern } from "./ast.js";
66
import { NUMBER, DATETIME } from "./builtins.js";
77
import { getMemoizerForLocale, IntlCache } from "./memoizer.js";
88

99
export type TextTransform = (text: string) => string;
10-
export type FluentVariable = FluentValue | string | number | Date;
1110

1211
/**
1312
* Message bundles are single-language stores of translation resources. They are

fluent-bundle/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
*/
99

1010
export type { Message } from "./ast.js";
11-
export { FluentBundle, FluentVariable, TextTransform } from "./bundle.js";
11+
export { FluentBundle, TextTransform } from "./bundle.js";
1212
export { FluentResource } from "./resource.js";
1313
export type { Scope } from "./scope.js";
1414
export {
1515
FluentValue,
16+
FluentVariable,
1617
FluentType,
1718
FluentFunction,
1819
FluentNone,

fluent-bundle/src/resolver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
FluentNone,
3131
FluentNumber,
3232
FluentDateTime,
33+
FluentVariable,
3334
} from "./types.js";
3435
import { Scope } from "./scope.js";
3536
import {
@@ -44,7 +45,6 @@ import {
4445
ComplexPattern,
4546
Pattern,
4647
} from "./ast.js";
47-
import { FluentVariable } from "./bundle.js";
4848

4949
/**
5050
* The maximum number of placeables which can be expanded in a single call to
@@ -187,8 +187,8 @@ function resolveVariableReference(
187187
case "number":
188188
return new FluentNumber(arg);
189189
case "object":
190-
if (arg instanceof Date) {
191-
return new FluentDateTime(arg.getTime());
190+
if (FluentDateTime.supportsValue(arg)) {
191+
return new FluentDateTime(arg);
192192
}
193193
// eslint-disable-next-line no-fallthrough
194194
default:

fluent-bundle/src/scope.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { FluentBundle, FluentVariable } from "./bundle.js";
1+
import { FluentBundle } from "./bundle.js";
22
import { ComplexPattern } from "./ast.js";
3+
import { FluentVariable } from "./types.js";
34

45
export class Scope {
56
/** The bundle for which the given resolution is happening. */

fluent-bundle/src/types.ts

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import { Scope } from "./scope.js";
2+
import type { Temporal } from "temporal-polyfill";
23

34
export type FluentValue = FluentType<unknown> | string;
45

6+
export type FluentVariable =
7+
| FluentValue
8+
| Temporal.Instant
9+
| Temporal.PlainDateTime
10+
| Temporal.PlainDate
11+
| Temporal.PlainTime
12+
| Temporal.PlainYearMonth
13+
| Temporal.PlainMonthDay
14+
| Temporal.ZonedDateTime
15+
| string
16+
| number;
17+
518
export type FluentFunction = (
619
positional: Array<FluentValue>,
720
named: Record<string, FluentValue>
@@ -104,37 +117,134 @@ export class FluentNumber extends FluentType<number> {
104117
/**
105118
* A `FluentType` representing a date and time.
106119
*
107-
* A `FluentDateTime` instance stores the number value of the date it
108-
* represents, as a numerical timestamp in milliseconds. It may also store an
120+
* A `FluentDateTime` instance stores a Date object, Temporal object, or a number
121+
* as a numerical timestamp in milliseconds. It may also store an
109122
* option bag of options which will be passed to `Intl.DateTimeFormat` when the
110123
* `FluentDateTime` is formatted to a string.
111124
*/
112-
export class FluentDateTime extends FluentType<number> {
125+
export class FluentDateTime extends FluentType<
126+
| number
127+
| Date
128+
| Temporal.Instant
129+
| Temporal.PlainDateTime
130+
| Temporal.PlainDate
131+
| Temporal.PlainMonthDay
132+
| Temporal.PlainTime
133+
| Temporal.PlainYearMonth
134+
| Temporal.ZonedDateTime
135+
> {
113136
/** Options passed to `Intl.DateTimeFormat`. */
114137
public opts: Intl.DateTimeFormatOptions;
115138

139+
static supportsValue(value: any): value is ConstructorParameters<typeof Temporal.Instant>[0] {
140+
if (typeof value === "number") return true;
141+
if (value instanceof Date) return true;
142+
if (value instanceof FluentType) return FluentDateTime.supportsValue(value.valueOf());
143+
// Temporary workaround to support environments without Temporal
144+
if ('Temporal' in globalThis) {
145+
if (
146+
// @ts-ignore
147+
value instanceof Temporal.Instant || // @ts-ignore
148+
value instanceof Temporal.PlainDateTime || // @ts-ignore
149+
value instanceof Temporal.PlainDate || // @ts-ignore
150+
value instanceof Temporal.PlainMonthDay || // @ts-ignore
151+
value instanceof Temporal.PlainTime || // @ts-ignore
152+
value instanceof Temporal.PlainYearMonth || // @ts-ignore
153+
value instanceof Temporal.ZonedDateTime
154+
) {
155+
return true;
156+
}
157+
}
158+
return false
159+
}
160+
116161
/**
117162
* Create an instance of `FluentDateTime` with options to the
118163
* `Intl.DateTimeFormat` constructor.
119164
*
120165
* @param value The number value of this `FluentDateTime`, in milliseconds.
121166
* @param opts Options which will be passed to `Intl.DateTimeFormat`.
122167
*/
123-
constructor(value: number, opts: Intl.DateTimeFormatOptions = {}) {
168+
constructor(
169+
value:
170+
| number
171+
| Date
172+
| Temporal.Instant
173+
| Temporal.PlainDateTime
174+
| Temporal.PlainDate
175+
| Temporal.PlainMonthDay
176+
| Temporal.PlainTime
177+
| Temporal.PlainYearMonth
178+
| Temporal.ZonedDateTime
179+
| FluentDateTime
180+
| FluentType<number>,
181+
opts: Intl.DateTimeFormatOptions = {}
182+
) {
183+
// unwrap any FluentType value, but only retain the opts from FluentDateTime
184+
if (value instanceof FluentDateTime) {
185+
opts = { ...value.opts, ...opts };
186+
value = value.value;
187+
} else if (value instanceof FluentType) {
188+
value = value.valueOf();
189+
}
190+
191+
if (typeof value === "object") {
192+
// Intl.DateTimeFormat defaults to gregorian calendar, but Temporal defaults to iso8601
193+
if ('calendarId' in value) {
194+
if (opts.calendar === undefined) {
195+
opts = { ...opts, calendar: value.calendarId };
196+
} else if (opts.calendar !== value.calendarId && 'withCalendar' in value) {
197+
value = value.withCalendar(opts.calendar);
198+
}
199+
}
200+
201+
// Temporal.ZonedDateTime is timezone aware
202+
if ('timeZoneId' in value) {
203+
if (opts.timeZone === undefined) {
204+
opts = { ...opts, timeZone: value.timeZoneId };
205+
} else if (opts.timeZone !== value.timeZoneId && 'withTimeZone' in value) {
206+
value = value.withTimeZone(opts.timeZone);
207+
}
208+
}
209+
210+
// Temporal.ZonedDateTime cannot be formatted directly
211+
if ('toInstant' in value) {
212+
value = value.toInstant();
213+
}
214+
}
215+
124216
super(value);
125217
this.opts = opts;
126218
}
127219

220+
/**
221+
* Convert this `FluentDateTime` to a number.
222+
* Note that this isn't always possible due to the nature of Temporal objects.
223+
* In such cases, a TypeError will be thrown.
224+
*/
225+
toNumber(): number {
226+
const value = this.value;
227+
if (typeof value === "number") return value;
228+
if (value instanceof Date) return value.getTime();
229+
if ('epochMilliseconds' in value) return value.epochMilliseconds;
230+
if ('toZonedDateTime' in value) return (value as Temporal.PlainDateTime).toZonedDateTime("UTC").epochMilliseconds;
231+
throw new TypeError("Unwrapping a non-number value as a number");
232+
}
233+
128234
/**
129235
* Format this `FluentDateTime` to a string.
130236
*/
131237
toString(scope: Scope): string {
132238
try {
133239
const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
134-
return dtf.format(this.value);
240+
return dtf.format(this.value as Parameters<Intl.DateTimeFormat["format"]>[0]);
135241
} catch (err) {
136242
scope.reportError(err);
137-
return new Date(this.value).toISOString();
243+
if (typeof this.value === "number" || this.value instanceof Date) {
244+
return new Date(this.value).toISOString();
245+
} else {
246+
return this.value.toString();
247+
}
138248
}
139249
}
140250
}

0 commit comments

Comments
 (0)