Skip to content

[Compatibility Hazard] ECMA 402 2nd Edition Changed the [[Call]] Behavior of Intl Constructors #57

@ericf

Description

@ericf

The 1st Edition of ECMA 402 specified the [[Call]] behavior for Intl constructors; e.g. Intl.DateTimeFormat.call(this [, locales [, options]]) to return the this context object that was passed-in. In the 2nd Edition, the [[Call]] behavior no longer states that the passed-in context object should be retuned. This change is a potential compatibility hazard.

As the developer and maintainer of the popular FormatJS i18n libraries, I've begun receiving issues from developers testing Chrome Canary (49) that their dates and numbers were failing for format, causing an Error to be thrown.

The high-level framework integration libs that are part of FormatJS — react-intl, ember-intl, handlebars-intl — are used by many web apps, including many of Yahoo's web apps. All these libraries use the underlying intl-format-cache which memoizes the Intl constructors because they are expensive to create. The memoization technique essentially does the following:

function constructIntlInstance(IntlConstructor) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        var instance = Object.create(IntlConstructor.prototype);
        IntlConstructor.apply(instance, args);
        return instance;
    };
}

Note: That this code depends on the following invariant:

var instance = Object.create(IntlConstructor.prototype);
instance === IntlConstructor.call(instance); // true

It is dependent on the the Intl constructors being .call()-able and the returning the context object passed-in. This .call() behavior for the Intl constructors is supported in all ECMA 402 1st Edition implementations.

After receiving an issue report about this code causing an Error to be thrown Chrome Canary (49), I dug in and found this recent V8 change which updates V8's implementation to match ECMA 402 2nd Edition, thus removing the code that returns the passed-in context object when the Intl constructors are .call()-ed.

Today, I've released intl-format-cache@2.0.5 which changes the memoization implementation to make sure the [[Construct]] behavior always happens by invoking the Intl constructors with new. Essentially doing the following:

function constructIntlInstance(IntlConstructor) {
    return (...args) => new IntlConstructor(...args);
}

In ES5 an equivalent would be:

function constructIntlInstance(IntlConstructor) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        return new (Function.prototype.bind.apply(IntlConstructor, [null].concat(args)))();
    };
}

While this issue is now "fixed" in intl-format-cache, developers must upgrade their dependencies and re-deploy their apps. I will help to communicate this change, but I'm worried that removing the 1st Edition [[Call]] behavior will break many apps/sites 😞

How should we move forward to prevent end-users from having broken experiences?

Edited based on @rwaldron's feedback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions