Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion fluent-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"npm": ">=7.0.0"
},
"devDependencies": {
"@fluent/dedent": "^0.5.0"
"@fluent/dedent": "^0.5.0",
"temporal-polyfill": "^0.2.5"
}
}
15 changes: 3 additions & 12 deletions fluent-bundle/src/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}
Expand Down Expand Up @@ -157,17 +157,8 @@ export function DATETIME(
return new FluentNone(`DATETIME(${arg.valueOf()})`);
}

if (arg instanceof FluentDateTime) {
return new FluentDateTime(arg.valueOf(), {
...arg.opts,
...values(opts, DATETIME_ALLOWED),
});
}

if (arg instanceof FluentNumber) {
return new FluentDateTime(arg.valueOf(), {
...values(opts, DATETIME_ALLOWED),
});
if (arg instanceof FluentDateTime || arg instanceof FluentNumber) {
return new FluentDateTime(arg, values(opts, DATETIME_ALLOWED));
}

throw new TypeError("Invalid argument to DATETIME");
Expand Down
3 changes: 1 addition & 2 deletions fluent-bundle/src/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { resolveComplexPattern } from "./resolver.js";
import { Scope } from "./scope.js";
import { FluentResource } from "./resource.js";
import { FluentValue, FluentNone, FluentFunction } from "./types.js";
import { FluentVariable, FluentNone, FluentFunction } from "./types.js";
import { Message, Term, Pattern } from "./ast.js";
import { NUMBER, DATETIME } from "./builtins.js";
import { getMemoizerForLocale, IntlCache } from "./memoizer.js";

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

/**
* Message bundles are single-language stores of translation resources. They are
Expand Down
3 changes: 2 additions & 1 deletion fluent-bundle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
*/

export type { Message } from "./ast.js";
export { FluentBundle, FluentVariable, TextTransform } from "./bundle.js";
export { FluentBundle, TextTransform } from "./bundle.js";
export { FluentResource } from "./resource.js";
export type { Scope } from "./scope.js";
export {
FluentValue,
FluentVariable,
FluentType,
FluentFunction,
FluentNone,
Expand Down
6 changes: 3 additions & 3 deletions fluent-bundle/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
FluentNone,
FluentNumber,
FluentDateTime,
FluentVariable,
} from "./types.js";
import { Scope } from "./scope.js";
import {
Expand All @@ -44,7 +45,6 @@ import {
ComplexPattern,
Pattern,
} from "./ast.js";
import { FluentVariable } from "./bundle.js";

/**
* The maximum number of placeables which can be expanded in a single call to
Expand Down Expand Up @@ -187,8 +187,8 @@ function resolveVariableReference(
case "number":
return new FluentNumber(arg);
case "object":
if (arg instanceof Date) {
return new FluentDateTime(arg.getTime());
if (FluentDateTime.supportsValue(arg)) {
return new FluentDateTime(arg);
}
// eslint-disable-next-line no-fallthrough
default:
Expand Down
3 changes: 2 additions & 1 deletion fluent-bundle/src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FluentBundle, FluentVariable } from "./bundle.js";
import { FluentBundle } from "./bundle.js";
import { ComplexPattern } from "./ast.js";
import { FluentVariable } from "./types.js";

export class Scope {
/** The bundle for which the given resolution is happening. */
Expand Down
122 changes: 116 additions & 6 deletions fluent-bundle/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { Scope } from "./scope.js";
import type { Temporal } from "temporal-polyfill";

export type FluentValue = FluentType<unknown> | string;

export type FluentVariable =
| FluentValue
| Temporal.Instant
| Temporal.PlainDateTime
| Temporal.PlainDate
| Temporal.PlainTime
| Temporal.PlainYearMonth
| Temporal.PlainMonthDay
| Temporal.ZonedDateTime
| string
| number;

export type FluentFunction = (
positional: Array<FluentValue>,
named: Record<string, FluentValue>
Expand Down Expand Up @@ -104,37 +117,134 @@ export class FluentNumber extends FluentType<number> {
/**
* A `FluentType` representing a date and time.
*
* A `FluentDateTime` instance stores the number value of the date it
* represents, as a numerical timestamp in milliseconds. It may also store an
* A `FluentDateTime` instance stores a Date object, Temporal object, or a number
* as a numerical timestamp in milliseconds. It may also store an
* option bag of options which will be passed to `Intl.DateTimeFormat` when the
* `FluentDateTime` is formatted to a string.
*/
export class FluentDateTime extends FluentType<number> {
export class FluentDateTime extends FluentType<
| number
| Date
| Temporal.Instant
| Temporal.PlainDateTime
| Temporal.PlainDate
| Temporal.PlainMonthDay
| Temporal.PlainTime
| Temporal.PlainYearMonth
| Temporal.ZonedDateTime
> {
/** Options passed to `Intl.DateTimeFormat`. */
public opts: Intl.DateTimeFormatOptions;

static supportsValue(value: any): value is ConstructorParameters<typeof Temporal.Instant>[0] {
if (typeof value === "number") return true;
if (value instanceof Date) return true;
if (value instanceof FluentType) return FluentDateTime.supportsValue(value.valueOf());
// Temporary workaround to support environments without Temporal
if ('Temporal' in globalThis) {
if (
// @ts-ignore
value instanceof Temporal.Instant || // @ts-ignore
value instanceof Temporal.PlainDateTime || // @ts-ignore
value instanceof Temporal.PlainDate || // @ts-ignore
value instanceof Temporal.PlainMonthDay || // @ts-ignore
value instanceof Temporal.PlainTime || // @ts-ignore
value instanceof Temporal.PlainYearMonth || // @ts-ignore
value instanceof Temporal.ZonedDateTime
) {
return true;
}
}
return false
}

/**
* Create an instance of `FluentDateTime` with options to the
* `Intl.DateTimeFormat` constructor.
*
* @param value The number value of this `FluentDateTime`, in milliseconds.
* @param opts Options which will be passed to `Intl.DateTimeFormat`.
*/
constructor(value: number, opts: Intl.DateTimeFormatOptions = {}) {
constructor(
value:
| number
| Date
| Temporal.Instant
| Temporal.PlainDateTime
| Temporal.PlainDate
| Temporal.PlainMonthDay
| Temporal.PlainTime
| Temporal.PlainYearMonth
| Temporal.ZonedDateTime
| FluentDateTime
| FluentType<number>,
opts: Intl.DateTimeFormatOptions = {}
) {
// unwrap any FluentType value, but only retain the opts from FluentDateTime
if (value instanceof FluentDateTime) {
opts = { ...value.opts, ...opts };
value = value.value;
} else if (value instanceof FluentType) {
value = value.valueOf();
}

if (typeof value === "object") {
// Intl.DateTimeFormat defaults to gregorian calendar, but Temporal defaults to iso8601
if ('calendarId' in value) {
if (opts.calendar === undefined) {
opts = { ...opts, calendar: value.calendarId };
} else if (opts.calendar !== value.calendarId && 'withCalendar' in value) {
value = value.withCalendar(opts.calendar);
}
}

// Temporal.ZonedDateTime is timezone aware
if ('timeZoneId' in value) {
if (opts.timeZone === undefined) {
opts = { ...opts, timeZone: value.timeZoneId };
} else if (opts.timeZone !== value.timeZoneId && 'withTimeZone' in value) {
value = value.withTimeZone(opts.timeZone);
}
}

// Temporal.ZonedDateTime cannot be formatted directly
if ('toInstant' in value) {
value = value.toInstant();
}
}

super(value);
this.opts = opts;
}

/**
* Convert this `FluentDateTime` to a number.
* Note that this isn't always possible due to the nature of Temporal objects.
* In such cases, a TypeError will be thrown.
*/
toNumber(): number {
const value = this.value;
if (typeof value === "number") return value;
if (value instanceof Date) return value.getTime();
if ('epochMilliseconds' in value) return value.epochMilliseconds;
if ('toZonedDateTime' in value) return (value as Temporal.PlainDateTime).toZonedDateTime("UTC").epochMilliseconds;
throw new TypeError("Unwrapping a non-number value as a number");
}

/**
* Format this `FluentDateTime` to a string.
*/
toString(scope: Scope): string {
try {
const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
return dtf.format(this.value);
return dtf.format(this.value as Parameters<Intl.DateTimeFormat["format"]>[0]);
} catch (err) {
scope.reportError(err);
return new Date(this.value).toISOString();
if (typeof this.value === "number" || this.value instanceof Date) {
return new Date(this.value).toISOString();
} else {
return this.value.toString();
}
}
}
}
Loading
Loading