diff --git a/fluent/src/builtins.js b/fluent/src/builtins.js index ebd47b671..a91dda881 100644 --- a/fluent/src/builtins.js +++ b/fluent/src/builtins.js @@ -15,9 +15,9 @@ import { FluentNumber, FluentDateTime } from './types'; export default { 'NUMBER': ([arg], opts) => - new FluentNumber(arg.value, merge(arg.opts, opts)), + new FluentNumber(value(arg), merge(arg.opts, opts)), 'DATETIME': ([arg], opts) => - new FluentDateTime(arg.value, merge(arg.opts, opts)), + new FluentDateTime(value(arg), merge(arg.opts, opts)), }; function merge(argopts, opts) { @@ -26,8 +26,15 @@ function merge(argopts, opts) { function values(opts) { const unwrapped = {}; - for (const name of Object.keys(opts)) { - unwrapped[name] = opts[name].value; + for (const [name, opt] of Object.entries(opts)) { + unwrapped[name] = value(opt); } return unwrapped; } + +function value(arg) { + // StringExpression-typed options are parsed as regular strings by the + // runtime parser and are not converted to a FluentType by the resolver. + // They don't have the "value" property; they are the value. + return typeof arg === 'string' ? arg : arg.value; +} diff --git a/fluent/src/resolver.js b/fluent/src/resolver.js index 02e4a9d99..473d9a2b5 100644 --- a/fluent/src/resolver.js +++ b/fluent/src/resolver.js @@ -457,7 +457,7 @@ function CallExpression(env, {fun, args}) { } const posargs = []; - const keyargs = []; + const keyargs = {}; for (const arg of args) { if (arg.type === 'narg') { @@ -467,8 +467,12 @@ function CallExpression(env, {fun, args}) { } } - // XXX functions should also report errors - return callee(posargs, keyargs); + try { + return callee(posargs, keyargs); + } catch (e) { + // XXX Report errors. + return FluentNone(); + } } /** diff --git a/fluent/src/types.js b/fluent/src/types.js index d88dae3b8..e1f9430c7 100644 --- a/fluent/src/types.js +++ b/fluent/src/types.js @@ -54,10 +54,15 @@ export class FluentNumber extends FluentType { } valueOf(ctx) { - const nf = ctx._memoizeIntlObject( - Intl.NumberFormat, this.opts - ); - return nf.format(this.value); + try { + const nf = ctx._memoizeIntlObject( + Intl.NumberFormat, this.opts + ); + return nf.format(this.value); + } catch (e) { + // XXX Report the error. + return this.value; + } } /** @@ -81,10 +86,15 @@ export class FluentDateTime extends FluentType { } valueOf(ctx) { - const dtf = ctx._memoizeIntlObject( - Intl.DateTimeFormat, this.opts - ); - return dtf.format(this.value); + try { + const dtf = ctx._memoizeIntlObject( + Intl.DateTimeFormat, this.opts + ); + return dtf.format(this.value); + } catch (e) { + // XXX Report the error. + return this.value; + } } } diff --git a/fluent/test/functions_builtin_test.js b/fluent/test/functions_builtin_test.js index 79ef00b92..fa9b5e501 100644 --- a/fluent/test/functions_builtin_test.js +++ b/fluent/test/functions_builtin_test.js @@ -6,46 +6,106 @@ import { MessageContext } from '../src/context'; import { ftl } from './util'; suite('Built-in functions', function() { - let ctx, args, errs; - - setup(function() { - errs = []; - }); + let ctx; suite('NUMBER', function(){ suiteSetup(function() { ctx = new MessageContext('en-US', { useIsolating: false }); ctx.addMessages(ftl` - foo = { NUMBER(1) } + num-decimal = { NUMBER($arg) } + num-percent = { NUMBER($arg, style: "percent") } + num-bad-opt = { NUMBER($arg, style: "bad") } `); }); - test('formats the number', function() { - const msg = ctx.getMessage('foo'); - const val = ctx.format(msg, args, errs); - assert.equal(val, '1'); - assert.equal(errs.length, 0); + test('missing argument', function() { + let msg; + + msg = ctx.getMessage('num-decimal'); + assert.equal(ctx.format(msg), 'NaN'); + + msg = ctx.getMessage('num-percent'); + assert.equal(ctx.format(msg), 'NaN'); + + msg = ctx.getMessage('num-bad-opt'); + assert.equal(ctx.format(msg), 'NaN'); + }); + + test('number argument', function() { + const args = {arg: 1}; + let msg; + + msg = ctx.getMessage('num-decimal'); + assert.equal(ctx.format(msg, args), '1'); + + msg = ctx.getMessage('num-percent'); + assert.equal(ctx.format(msg, args), '100%'); + + msg = ctx.getMessage('num-bad-opt'); + assert.equal(ctx.format(msg, args), '1'); + }); + + test('string argument', function() { + const args = {arg: "Foo"}; + let msg; + + msg = ctx.getMessage('num-decimal'); + assert.equal(ctx.format(msg, args), 'NaN'); + + msg = ctx.getMessage('num-percent'); + assert.equal(ctx.format(msg, args), 'NaN'); + + msg = ctx.getMessage('num-bad-opt'); + assert.equal(ctx.format(msg, args), 'NaN'); }); }); suite('DATETIME', function(){ - let dtf; - suiteSetup(function() { - dtf = new Intl.DateTimeFormat('en-US'); ctx = new MessageContext('en-US', { useIsolating: false }); ctx.addMessages(ftl` - foo = { DATETIME($date) } + dt-default = { DATETIME($arg) } + dt-month = { DATETIME($arg, month: "long") } + dt-bad-opt = { DATETIME($arg, month: "bad") } `); }); - test('formats the date', function() { + test('missing argument', function() { + let msg; + + msg = ctx.getMessage('dt-default'); + assert.equal(ctx.format(msg), 'Invalid Date'); + + msg = ctx.getMessage('dt-month'); + assert.equal(ctx.format(msg), 'Invalid Date'); + + msg = ctx.getMessage('dt-bad-opt'); + assert.equal(ctx.format(msg), 'Invalid Date'); + }); + + test.only('Date argument', function() { const date = new Date('2016-09-29'); - const msg = ctx.getMessage('foo'); - const val = ctx.format(msg, { date }, errs); // format the date argument to account for the testrunner's timezone - assert.equal(val, dtf.format(date)); - assert.equal(errs.length, 0); + const expectedDefault = + (new Intl.DateTimeFormat('en-US')).format(date); + const expectedMonth = + (new Intl.DateTimeFormat('en-US', {month: 'long'})).format(date); + + const args = {arg: date}; + let msg; + + msg = ctx.getMessage('dt-default'); + assert.equal(ctx.format(msg, args), expectedDefault); + + msg = ctx.getMessage('dt-month'); + assert.equal(ctx.format(msg, args), expectedMonth); + + msg = ctx.getMessage('dt-bad-opt'); + // The argument value will be coerced into a string by the join operation + // in MessageContext.format. The result looks something like this; it + // may vary depending on the TZ: + // Thu Sep 29 2016 02:00:00 GMT+0200 (CEST) + assert.equal(ctx.format(msg, args), date.toString()); }); }); });