diff --git a/fluent-bundle/src/bundle.ts b/fluent-bundle/src/bundle.ts index 3c0f7549..1a61b64a 100644 --- a/fluent-bundle/src/bundle.ts +++ b/fluent-bundle/src/bundle.ts @@ -5,6 +5,7 @@ import { FluentValue, FluentNone, FluentFunction } from "./types.js"; import { Message, Term, Pattern } from "./ast.js"; import { NUMBER, DATETIME } from "./builtins.js"; import { getMemoizerForLocale, IntlCache } from "./memoizer.js"; +import { defaultCaster, FluentCast, FluentCastRegistry } from "./cast.js"; export type TextTransform = (text: string) => string; export type FluentVariable = FluentValue | string | number | Date; @@ -28,6 +29,8 @@ export class FluentBundle { public _transform: TextTransform; /** @ignore */ public _intls: IntlCache; + /** @ignore */ + public _caster: FluentCastRegistry; /** * Create an instance of `FluentBundle`. @@ -52,10 +55,12 @@ export class FluentBundle { constructor( locales: string | Array, { + cast, functions, useIsolating = true, transform = (v: string): string => v, }: { + cast?: FluentCast; /** Additional functions available to translations as builtins. */ functions?: Record; /** @@ -77,6 +82,7 @@ export class FluentBundle { this._useIsolating = useIsolating; this._transform = transform; this._intls = getMemoizerForLocale(locales); + this._caster = new FluentCastRegistry(defaultCaster, cast); } /** @@ -157,6 +163,38 @@ export class FluentBundle { return errors; } + /** + * Adds a conversion rule for specific variable types. + * + * @example Conversion function for a custom class + * ```js + * class MyNumber { + * constructor(value) { this.value = value; } + * } + * + * bundle.addCast(MyNumber, (num) => new FluentNumber(num.value)); + * ``` + * + * @example Registering a custom type + * ```js + * // Custom type to format Intl.Locale instances + * class FluentLanguage extends FluentType { + * toString(scope) { + * const dnf = scope.memoizeIntlObject(Intl.DisplayNames, { type: "language" }); + * return dnf.of(this.value); + * } + * } + * + * // Register for automatic conversion + * bundle.addCast(Intl.Locale, FluentLanguage); + * ``` + * + * @param args see {@link FluentCastRegistry.prototype.add} + */ + addCast(...args: Parameters): void { + this._caster.add(...args); + } + /** * Format a `Pattern` to a string. * diff --git a/fluent-bundle/src/cast.ts b/fluent-bundle/src/cast.ts new file mode 100644 index 00000000..0f717a3d --- /dev/null +++ b/fluent-bundle/src/cast.ts @@ -0,0 +1,104 @@ +import { FluentNumber, FluentDateTime, FluentType, FluentValue } from "./types.js"; + +type Class = new (...args: any[]) => T; +type Guard = (value: unknown) => boolean + +export type FluentTypeClass = Class>; +export type FluentTypeFunction = (value: T) => FluentValue | undefined; +export type FluentTypeCast = FluentTypeClass | FluentTypeFunction; +export type FluentCast = FluentTypeFunction | FluentCaster + +function generateGuard(guardValue: any): Guard { + switch (typeof guardValue) { + case "function": + return (value: unknown) => value instanceof guardValue; + case "string": + return (value: unknown) => typeof value === guardValue; + default: + return (value: unknown) => value === guardValue; + } +} + +/** + * Abstract class for implementing a type casting. + * @see {@link FluentCastRegistry} for the default implementation. + */ +export abstract class FluentCaster { + abstract castValue(value: unknown): FluentValue | undefined +} + +export class FluentCastRegistry extends FluentCaster { + /** @ignore */ + public _casters: Array = []; + + /** @ignore */ + constructor(...args: Array) { + super(); + for (const arg of args) { + if(arg) this.add(arg); + } + } + + /** + * Register a new type casting rule for a specific class. + * + * @param rawType class or type that will be converted into a Fluent value + * @param fluentType either a function called for casting or a FluentType class + */ + add(rawType: Class | string, fluentType: FluentTypeCast): void; + + /** + * Register a new type casting rule tried out for every value. + * + * @param caster either a function called for casting or a FluentCaster instance + */ + add(caster: FluentCast): void; + + add(...args: any[]): void { + let caster: FluentTypeCast | FluentCaster; + let guard: Guard | undefined; + + if (args.length === 1) { + caster = args[0]; + } else if (args.length === 2) { + caster = args[1]; + guard = generateGuard(args[0]); + } else { + throw new Error("Invalid arguments"); + } + + if (caster instanceof FluentCaster) { + caster = caster.castValue.bind(caster); + } else if (caster.prototype instanceof FluentType) { + const fluentTypeClass = caster as FluentTypeClass; + caster = (value: T) => new fluentTypeClass(value); + } + + if (guard !== undefined) { + const guarded = caster as FluentTypeFunction; + caster = (value: unknown) => guard!(value) ? guarded(value as T) : undefined; + } + + this._casters.unshift(caster as FluentTypeFunction); + } + + /** + * Casts an unknown value to a FluentValue. + * Returns `undefined` if the value cannot be cast. + */ + castValue(value: unknown): FluentValue | undefined { + for (const caster of this._casters) { + const result = caster(value); + if (result !== undefined) return result; + } + } +} + +/** + * Default FluentCaster with built-in types, used by every {@link FluentBundle}. + * Turns numbers into {@link FluentNumber} and dates into {@link FluentDateTime}. + */ +export const defaultCaster = new FluentCastRegistry(); + +defaultCaster.add("number", FluentNumber); +defaultCaster.add(Date, (value: Date) => new FluentDateTime(value.getTime())); diff --git a/fluent-bundle/src/index.ts b/fluent-bundle/src/index.ts index 66e59124..978b5209 100644 --- a/fluent-bundle/src/index.ts +++ b/fluent-bundle/src/index.ts @@ -11,6 +11,7 @@ export type { Message } from "./ast.js"; export { FluentBundle, FluentVariable, TextTransform } from "./bundle.js"; export { FluentResource } from "./resource.js"; export type { Scope } from "./scope.js"; +export { FluentCaster, FluentCastRegistry, defaultCaster } from "./cast.js"; export { FluentValue, FluentType, diff --git a/fluent-bundle/src/resolver.ts b/fluent-bundle/src/resolver.ts index 2e849bc1..485be68e 100644 --- a/fluent-bundle/src/resolver.ts +++ b/fluent-bundle/src/resolver.ts @@ -29,7 +29,6 @@ import { FluentType, FluentNone, FluentNumber, - FluentDateTime, } from "./types.js"; import { Scope } from "./scope.js"; import { @@ -175,28 +174,18 @@ function resolveVariableReference( return new FluentNone(`$${name}`); } - // Return early if the argument already is an instance of FluentType. - if (arg instanceof FluentType) { + if (arg instanceof FluentType || typeof arg === "string") { return arg; } - // Convert the argument to a Fluent type. - switch (typeof arg) { - case "string": - return arg; - case "number": - return new FluentNumber(arg); - case "object": - if (arg instanceof Date) { - return new FluentDateTime(arg.getTime()); - } - // eslint-disable-next-line no-fallthrough - default: - scope.reportError( - new TypeError(`Variable type not supported: $${name}, ${typeof arg}`) - ); - return new FluentNone(`$${name}`); - } + const result = scope.bundle._caster.castValue(arg); + if (result) return result; + + scope.reportError( + new TypeError(`Variable type not supported: $${name}, ${typeof arg}`) + ); + + return new FluentNone(`$${name}`); } /** Resolve a reference to another message. */ diff --git a/fluent-bundle/test/cast_test.js b/fluent-bundle/test/cast_test.js new file mode 100644 index 00000000..7a999f23 --- /dev/null +++ b/fluent-bundle/test/cast_test.js @@ -0,0 +1,240 @@ +"use strict"; + +import assert from "assert"; + +import { FluentBundle } from "../esm/bundle.js"; +import { FluentResource } from "../esm/resource.js"; +import { FluentCaster, FluentCastRegistry } from "../esm/cast.js"; +import { FluentType } from "../esm/types.js"; + +suite("Variable casting", function () { + class MyClass { + constructor(value = undefined) { + this.value = value; + } + } + + function myCast(value) { + if (value instanceof MyClass) { + return "custom value"; + } + } + + class MyCast extends FluentCaster { + castValue(value) { + if (value instanceof MyClass) { + return "custom value"; + } + } + } + + class MyType extends FluentType { + toString(scope) { + return "custom type"; + } + } + + let resource, bundle, errs, msg; + + suiteSetup(function () { + resource = new FluentResource("message = { $var }"); + }); + + setup(function () { + errs = []; + }); + + suite('cast option', function () { + suite('with a function', function () { + suiteSetup(function () { + bundle = new FluentBundle("en-US", { + cast: myCast + }); + bundle.addResource(resource); + msg = bundle.getMessage("message"); + }); + + test("uses custom casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new MyClass() }, errs); + assert.strictEqual(val, "custom value"); + assert.strictEqual(errs.length, 0); + }); + + test("still uses default casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new Date("1993-02-02") }, errs); + assert.strictEqual(val, "2/2/1993"); + assert.strictEqual(errs.length, 0); + }); + + test("causes an error if the value cannot be casted", function () { + const val = bundle.formatPattern(msg.value, { var: {} }, errs); + assert.strictEqual(val, "{$var}"); + assert.strictEqual(errs.length, 1); + assert(errs[0] instanceof TypeError); + }); + }); + + suite('with a FluentCaster', function () { + suiteSetup(function () { + bundle = new FluentBundle("en-US", { + cast: new MyCast() + }); + bundle.addResource(resource); + msg = bundle.getMessage("message"); + }); + + test("uses custom casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new MyClass() }, errs); + assert.strictEqual(val, "custom value"); + assert.strictEqual(errs.length, 0); + }); + + test("still uses default casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new Date("1993-02-02") }, errs); + assert.strictEqual(val, "2/2/1993"); + assert.strictEqual(errs.length, 0); + }); + + test("causes an error if the value cannot be casted", function () { + const val = bundle.formatPattern(msg.value, { var: {} }, errs); + assert.strictEqual(val, "{$var}"); + assert.strictEqual(errs.length, 1); + assert(errs[0] instanceof TypeError); + }); + }) + }); + + suite('addCast', function () { + suite('with a class and a function', function () { + suiteSetup(function () { + bundle = new FluentBundle("en-US"); + bundle.addCast(MyClass, function () { + return "custom value" + }); + bundle.addResource(resource); + msg = bundle.getMessage("message"); + }); + + test("uses custom casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new MyClass() }, errs); + assert.strictEqual(val, "custom value"); + assert.strictEqual(errs.length, 0); + }); + + test("still uses default casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new Date("1993-02-02") }, errs); + assert.strictEqual(val, "2/2/1993"); + assert.strictEqual(errs.length, 0); + }); + + test("causes an error if the value cannot be casted", function () { + const val = bundle.formatPattern(msg.value, { var: {} }, errs); + assert.strictEqual(val, "{$var}"); + assert.strictEqual(errs.length, 1); + assert(errs[0] instanceof TypeError); + }); + }); + + suite('with a class and a type', function () { + suiteSetup(function () { + bundle = new FluentBundle("en-US"); + bundle.addCast(MyClass, MyType); + bundle.addResource(resource); + msg = bundle.getMessage("message"); + }); + + test("uses custom casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new MyClass() }, errs); + assert.strictEqual(val, "custom type"); + assert.strictEqual(errs.length, 0); + }); + + test("still uses default casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new Date("1993-02-02") }, errs); + assert.strictEqual(val, "2/2/1993"); + assert.strictEqual(errs.length, 0); + }); + + test("causes an error if the value cannot be casted", function () { + const val = bundle.formatPattern(msg.value, { var: {} }, errs); + assert.strictEqual(val, "{$var}"); + assert.strictEqual(errs.length, 1); + assert(errs[0] instanceof TypeError); + }); + }); + + suite('with a function', function () { + suiteSetup(function () { + bundle = new FluentBundle("en-US"); + bundle.addCast(myCast); + bundle.addResource(resource); + msg = bundle.getMessage("message"); + }); + + test("uses custom casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new MyClass() }, errs); + assert.strictEqual(val, "custom value"); + assert.strictEqual(errs.length, 0); + }); + + test("still uses default casting function", function () { + const val = bundle.formatPattern(msg.value, { var: new Date("1993-02-02") }, errs); + assert.strictEqual(val, "2/2/1993"); + assert.strictEqual(errs.length, 0); + }); + + test("causes an error if the value cannot be casted", function () { + const val = bundle.formatPattern(msg.value, { var: {} }, errs); + assert.strictEqual(val, "{$var}"); + assert.strictEqual(errs.length, 1); + assert(errs[0] instanceof TypeError); + }); + }); + }); + + suite("FluentCastRegistry", function () { + let registry; + let object = { foo: "bar" }; + + suiteSetup(function () { + registry = new FluentCastRegistry(); + registry.add(MyClass, MyType); + registry.add("boolean", function () { + return "true/false" + }); + registry.add(function (value) { + if (typeof value === "number" && value % 2 === 0) { + return "even"; + } + }); + registry.add(object, function () { + return "custom object"; + }) + }); + + test("matches on class", function () { + const result = registry.castValue(new MyClass()); + assert(result instanceof MyType); + }); + + test("matches on type", function () { + const result = registry.castValue(true); + assert.strictEqual(result, "true/false"); + }); + + test("matches on function", function () { + const result = registry.castValue(2); + assert.strictEqual(result, "even"); + }); + + test("matches on object", function () { + const result = registry.castValue(object); + assert.strictEqual(result, "custom object"); + }); + + test("returns undefined if no match", function () { + const result = registry.castValue(1); + assert.strictEqual(result, undefined); + }); + }); +}); \ No newline at end of file diff --git a/fluent-temporal/.gitignore b/fluent-temporal/.gitignore new file mode 100644 index 00000000..0bd1f3fc --- /dev/null +++ b/fluent-temporal/.gitignore @@ -0,0 +1,2 @@ +esm/ +/index.js diff --git a/fluent-temporal/.npmignore b/fluent-temporal/.npmignore new file mode 100644 index 00000000..abad613f --- /dev/null +++ b/fluent-temporal/.npmignore @@ -0,0 +1,7 @@ +.nyc_output +coverage +esm/.compiled +src +test +makefile +tsconfig.json diff --git a/fluent-temporal/README.md b/fluent-temporal/README.md new file mode 100644 index 00000000..f5b92710 --- /dev/null +++ b/fluent-temporal/README.md @@ -0,0 +1,64 @@ +# @fluent/temporal ![](https://github.com/projectfluent/fluent.js/workflows/test/badge.svg) + +`@fluent/temporal` adds support for [Temporal][] objects to Fluent.js. + +The Temporal standard is considered experimental, and support is still limited. +Various [polyfills][] are available. + +Once the Temporal standard is more widely supported, this package may be integrated into `@fluent/bundle` itself. + +[temporal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal +[polyfills]: https://github.com/fullcalendar/temporal-polyfill + +## Installation + +`@fluent/bundle` can be used both on the client-side and the server-side. You +can install it from the npm registry: + + npm install @fluent/temporal + + +## How to use + +### With global `Temporal` object + +If you're in an invironment that already supports the `Temporal` object, or you have a global polyfill enabled, all you have to do is import the package: + +```javascript +import "@fluent/temporal"; +``` + +### With a local polyfill + +If you use a polyfill that doesn't add a global `Temporal` object, you can explicitly add `FluentTemporal` to your bundle: + +```javascript +import { FluentBundle } from "@fluent/bundle"; +import { FluentTemporal } from "@fluent/temporal"; +import { Temporal } from 'temporal-polyfill'; + +const bundle = new FluentBundle("en-US", { + cast: new FluentTemporal(Temporal) +}); +``` + +## Supported Temporal objects + +The following Temporal objects are supported: + +* [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) + * Calendar and timezone will be preserved + * Converted into a `FluentDateTime` object (from `@fluent/bundle`) + * Can be passed to built-in `DATETIME` functions in Fluent messages +* [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) + * Converted into a `FluentDateTime` object (from `@fluent/bundle`) + * Can be passed to built-in `DATETIME` functions in Fluent messages + +The following objects are not yet supported: + +* [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) +* [`Temporal.PlainDate`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate) +* [`Temporal.PlainDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime) +* [`Temporal.PlainMonthDay`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay) +* [`Temporal.PlainTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime) +* [`Temporal.PlainYearMonth`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth) diff --git a/fluent-temporal/package.json b/fluent-temporal/package.json new file mode 100644 index 00000000..3f5309b8 --- /dev/null +++ b/fluent-temporal/package.json @@ -0,0 +1,56 @@ +{ + "name": "@fluent/temporal", + "description": "Experimental Temporal support for Project Fluent", + "version": "0.1.0", + "homepage": "https://projectfluent.org", + "author": "Mozilla ", + "license": "Apache-2.0", + "contributors": [ + { + "name": "Konstantin Haase", + "email": "github@rkh.im" + } + ], + "main": "./index.js", + "module": "./esm/index.js", + "types": "./esm/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/projectfluent/fluent.js.git" + }, + "keywords": [ + "localization", + "l10n", + "internationalization", + "i18n", + "ftl", + "plural", + "gender", + "locale", + "language", + "formatting", + "translate", + "translation", + "format", + "temporal", + "time" + ], + "scripts": { + "build": "tsc", + "postbuild": "rollup -c ../rollup.config.mjs", + "docs": "typedoc --options ../typedoc.config.cjs", + "test": "mocha 'test/*_test.js'" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@fluent/bundle": "^0.18.0" + }, + "devDependencies": { + "@fluent/bundle": "^0.18.0", + "@fluent/dedent": "^0.5.0", + "temporal-polyfill": "^0.2.5" + } +} diff --git a/fluent-temporal/src/index.ts b/fluent-temporal/src/index.ts new file mode 100644 index 00000000..2a6111e7 --- /dev/null +++ b/fluent-temporal/src/index.ts @@ -0,0 +1,46 @@ +import { + FluentCaster, FluentCastRegistry, defaultCaster, FluentValue, FluentDateTime +} from "@fluent/bundle"; + +/** + * FluentCaster implementation for Temporal objects. + */ +export class FluentTemporal extends FluentCaster { + private registry = new FluentCastRegistry(); + + /* + * Create a new FluentTemporal instance. + * @param temporal - Temporal namespace. + */ + constructor(temporal: typeof Temporal = Temporal) { + super(); + register(temporal, this.registry); + } + + /** @ignore */ + castValue(value: unknown): FluentValue | undefined { + return this.registry.castValue(value); + } +} + +function register(temporal: typeof Temporal, registry: FluentCastRegistry) { + registry.add(temporal.ZonedDateTime, (value: Temporal.ZonedDateTime) => { + const opts: Intl.DateTimeFormatOptions = { + timeZone: value.timeZoneId + } + + if (value.calendarId !== "iso8601") { + opts.calendar = value.calendarId; + } + + return new FluentDateTime(value.epochMilliseconds, opts); + }); + + registry.add(temporal.Instant, (value: Temporal.Instant) => { + return new FluentDateTime(value.epochMilliseconds); + }); +} + +if (typeof Temporal !== "undefined") { + register(Temporal, defaultCaster); +} diff --git a/fluent-temporal/src/temporal.d.ts b/fluent-temporal/src/temporal.d.ts new file mode 100644 index 00000000..46b9b149 --- /dev/null +++ b/fluent-temporal/src/temporal.d.ts @@ -0,0 +1,15 @@ +export {} + +declare global { + namespace Temporal { + class ZonedDateTime { + epochMilliseconds: number; + timeZoneId: string; + calendarId: string; + } + + class Instant { + epochMilliseconds: number; + } + } +} diff --git a/fluent-temporal/test/bundle_test.js b/fluent-temporal/test/bundle_test.js new file mode 100644 index 00000000..8184ced0 --- /dev/null +++ b/fluent-temporal/test/bundle_test.js @@ -0,0 +1,47 @@ +"use strict"; + +import assert from "assert"; +import ftl from "@fluent/dedent"; +import { FluentBundle, FluentResource } from "@fluent/bundle"; + +import { Temporal, caster } from "./utils.js"; + +suite('With FluentBundle', function () { + let bundle, errs, args; + + suiteSetup(function () { + bundle = new FluentBundle("en-US", { cast: caster }); + bundle.addResource( + new FluentResource(ftl` + no-formatting = { $arg } + with-formatting = { DATETIME($arg, month: "long", year: "numeric", day: "numeric") } + `) + ); + }); + + setup(function () { + errs = []; + }); + + suite("Temporal.ZonedDateTime", function () { + suiteSetup(function () { + // date chosen so that it would be a different day in UTC + const dateTime = "2025-01-01T18:00:00-06:00[America/Chicago]"; + args = { arg: Temporal.ZonedDateTime.from(dateTime) }; + }); + + test("without extra formatting", function () { + const msg = bundle.getMessage("no-formatting"); + const val = bundle.formatPattern(msg.value, args, errs); + assert.strictEqual(val, "1/1/2025"); + assert.strictEqual(errs.length, 0); + }); + + test("with extra formatting", function () { + const msg = bundle.getMessage("with-formatting"); + const val = bundle.formatPattern(msg.value, args, errs); + assert.strictEqual(val, "January 1, 2025"); + assert.strictEqual(errs.length, 0); + }); + }); +}); diff --git a/fluent-temporal/test/instant_test.js b/fluent-temporal/test/instant_test.js new file mode 100644 index 00000000..5da93331 --- /dev/null +++ b/fluent-temporal/test/instant_test.js @@ -0,0 +1,31 @@ +"use strict"; + +import assert from "assert"; + +import { Temporal, caster } from "./utils.js"; +import { FluentDateTime } from "@fluent/bundle"; + +suite("Temporal.Instant", function () { + let time, cast; + + suiteSetup(function () { + time = new Temporal.Instant(123456789n); + cast = caster.castValue(time); + }); + + test("Generates a FluentDateTime", function () { + assert(cast instanceof FluentDateTime); + }); + + test("Sets milliseconds correctly", function () { + assert.strictEqual(cast.value, 123); + }); + + test("Does not set timeZone", function () { + assert.strictEqual(cast.opts.timeZone, undefined); + }); + + test("Does not set calendar", function () { + assert.strictEqual(cast.opts.calendar, undefined); + }); +}); diff --git a/fluent-temporal/test/package.json b/fluent-temporal/test/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/fluent-temporal/test/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/fluent-temporal/test/utils.js b/fluent-temporal/test/utils.js new file mode 100644 index 00000000..852f100c --- /dev/null +++ b/fluent-temporal/test/utils.js @@ -0,0 +1,5 @@ +import { Temporal } from 'temporal-polyfill'; +import { FluentTemporal } from "../esm/index.js"; + +export const caster = new FluentTemporal(Temporal); +export { Temporal, FluentTemporal }; diff --git a/fluent-temporal/test/zoned_date_time_test.js b/fluent-temporal/test/zoned_date_time_test.js new file mode 100644 index 00000000..5948d81f --- /dev/null +++ b/fluent-temporal/test/zoned_date_time_test.js @@ -0,0 +1,36 @@ +"use strict"; + +import assert from "assert"; + +import { Temporal, caster } from "./utils.js"; +import { FluentDateTime } from "@fluent/bundle"; + +suite("Temporal.ZonedDateTime", function () { + let time, cast; + + suiteSetup(function () { + time = Temporal.ZonedDateTime.from("1970-01-01T00:00:00Z[Etc/UTC]"); + cast = caster.castValue(time); + }); + + test("Generates a FluentDateTime", function () { + assert(cast instanceof FluentDateTime); + }); + + test("Sets milliseconds correctly", function () { + assert.strictEqual(cast.value, 0); + }); + + test("Set timeZone correctly", function () { + assert.strictEqual(cast.opts.timeZone, "Etc/UTC"); + }); + + test("Does not set calendar if it is iso8601", function () { + assert.strictEqual(cast.opts.calendar, undefined); + }); + + test("Sets calendar if it is not iso8601", function () { + const islamic = caster.castValue(time.withCalendar("islamic")); + assert.strictEqual(islamic.opts.calendar, "islamic"); + }); +}); diff --git a/fluent-temporal/tsconfig.json b/fluent-temporal/tsconfig.json new file mode 100644 index 00000000..3e42c5be --- /dev/null +++ b/fluent-temporal/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./esm", + "rootDir": "./src" + }, + "include": [ + "./src/types/*.d.ts", + "./src/**/*.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 8aa3a975..a3e52368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "./fluent-langneg", "./fluent-react", "./fluent-syntax", - "./fluent-gecko" + "./fluent-gecko", + "./fluent-temporal" ], "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.1.0", @@ -151,6 +152,23 @@ "npm": ">=7.0.0" } }, + "fluent-temporal": { + "name": "@fluent/temporal", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@fluent/bundle": "^0.18.0", + "@fluent/dedent": "^0.5.0", + "temporal-polyfill": "^0.2.5" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@fluent/bundle": "^0.18.0" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -2326,6 +2344,10 @@ "resolved": "fluent-syntax", "link": true }, + "node_modules/@fluent/temporal": { + "resolved": "fluent-temporal", + "link": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -8304,6 +8326,23 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/temporal-polyfill": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.2.5.tgz", + "integrity": "sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "temporal-spec": "^0.2.4" + } + }, + "node_modules/temporal-spec": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.2.4.tgz", + "integrity": "sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==", + "dev": true, + "license": "ISC" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index f7b43fd4..e4d7dbcd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "./fluent-langneg", "./fluent-react", "./fluent-syntax", - "./fluent-gecko" + "./fluent-gecko", + "./fluent-temporal" ], "scripts": { "predist": "npm run clean", diff --git a/rollup.config.mjs b/rollup.config.mjs index c910f4cc..e3867031 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -8,6 +8,7 @@ const globalName = { "@fluent/react": "FluentReact", "@fluent/sequence": "FluentSequence", "@fluent/syntax": "FluentSyntax", + "@fluent/temporal": "FluentTemporal", }; export default async function () {