@@ -94,7 +94,7 @@ const testCases: Record<string, TestCase> = {
9494 msg : 'num-fraction-bad' ,
9595 scope : { arg : 1234 } ,
9696 exp : '{$arg}' ,
97- errors : [ 'oops ' ]
97+ errors : [ 'bad-option ' ]
9898 } ,
9999 { msg : 'num-style' , scope : { arg : 1234 } , exp : '123,400%' } ,
100100 { msg : 'num-currency' , scope : { arg : 1234 } , exp : '€1,234.00' } ,
@@ -163,23 +163,23 @@ const testCases: Record<string, TestCase> = {
163163 }
164164 ` ,
165165 tests : [
166- { msg : 'ref-attr' , exp : 'It' , errors : [ '$style' , 'not-selectable' ] } ,
166+ { msg : 'ref-attr' , exp : 'It' , errors : [ '$style' ] } ,
167167 {
168168 msg : 'ref-attr' ,
169169 scope : { style : 'chicago' } ,
170170 exp : 'It' ,
171- errors : [ '$style' , 'not-selectable' ]
171+ errors : [ '$style' ]
172172 } ,
173173 {
174174 msg : 'call-attr-no-args' ,
175175 exp : 'It' ,
176- errors : [ '$style' , 'not-selectable' ]
176+ errors : [ '$style' ]
177177 } ,
178178 {
179179 msg : 'call-attr-no-args' ,
180180 scope : { style : 'chicago' } ,
181181 exp : 'It' ,
182- errors : [ '$style' , 'not-selectable' ]
182+ errors : [ '$style' ]
183183 } ,
184184 { msg : 'call-attr-with-expected-arg' , exp : 'She' } ,
185185 {
@@ -190,13 +190,13 @@ const testCases: Record<string, TestCase> = {
190190 {
191191 msg : 'call-attr-with-other-arg' ,
192192 exp : 'It' ,
193- errors : [ '$style' , 'not-selectable' ]
193+ errors : [ '$style' ]
194194 } ,
195195 {
196196 msg : 'call-attr-with-other-arg' ,
197197 scope : { style : 'chicago' } ,
198198 exp : 'It' ,
199- errors : [ '$style' , 'not-selectable' ]
199+ errors : [ '$style' ]
200200 }
201201 ]
202202 } ,
@@ -254,7 +254,7 @@ const testCases: Record<string, TestCase> = {
254254 msg : 'select' ,
255255 scope : { } ,
256256 exp : 'B' ,
257- errors : [ '$selector' , 'not-selectable' ]
257+ errors : [ '$selector' ]
258258 } ,
259259 { msg : 'select' , scope : { selector : 'a' } , exp : 'A' } ,
260260 { msg : 'select' , scope : { selector : 'b' } , exp : 'B' } ,
@@ -263,7 +263,7 @@ const testCases: Record<string, TestCase> = {
263263 msg : 'number' ,
264264 scope : { } ,
265265 exp : 'B' ,
266- errors : [ '$selector' , 'not-selectable' ]
266+ errors : [ '$selector' ]
267267 } ,
268268 { msg : 'number' , scope : { selector : 0 } , exp : 'A' } ,
269269 { msg : 'number' , scope : { selector : 1 } , exp : 'B' } ,
@@ -272,18 +272,23 @@ const testCases: Record<string, TestCase> = {
272272 msg : 'plural' ,
273273 scope : { } ,
274274 exp : 'B' ,
275- errors : [ '$selector' , 'not-selectable' ]
275+ errors : [ '$selector' , 'bad-input' , ' not-selectable']
276276 } ,
277277 { msg : 'plural' , scope : { selector : 1 } , exp : 'A' } ,
278278 { msg : 'plural' , scope : { selector : 2 } , exp : 'B' } ,
279- { msg : 'plural' , scope : { selector : 'one' } , exp : 'A' } ,
279+ {
280+ msg : 'plural' ,
281+ scope : { selector : 'one' } ,
282+ exp : 'B' ,
283+ errors : [ 'bad-input' , 'not-selectable' ]
284+ } ,
280285 {
281286 msg : 'default' ,
282287 scope : { } ,
283288 exp : 'D' ,
284- errors : [ '$selector' , 'not-selectable' ]
289+ errors : [ '$selector' ]
285290 } ,
286- { msg : 'default' , scope : { selector : 1 } , exp : 'A ' } ,
291+ { msg : 'default' , scope : { selector : 1 } , exp : 'D ' } ,
287292 { msg : 'default' , scope : { selector : 2 } , exp : 'D' } ,
288293 { msg : 'default' , scope : { selector : 'one' } , exp : 'A' }
289294 ]
@@ -335,7 +340,14 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries(
335340 for ( const [ attr , mf ] of group ) {
336341 const { functions } = mf . resolvedOptions ( ) ;
337342 // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
338- validate ( data . get ( id ) ?. get ( attr ?? '' ) ! , functions ) ;
343+ const req = validate ( data . get ( id ) ?. get ( attr ?? '' ) ! , type => {
344+ throw new Error ( `Validation failed: ${ type } ` ) ;
345+ } ) ;
346+ for ( const fn of req . functions ) {
347+ if ( typeof functions [ fn ] !== 'function' ) {
348+ throw new Error ( `Unknown message function: ${ fn } ` ) ;
349+ }
350+ }
339351 }
340352 }
341353 } ) ;
@@ -352,17 +364,22 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries(
352364 const test_ = only ? test . only : test ;
353365 test_ ( name , ( ) => {
354366 const onError = jest . fn ( ) ;
355- const str = res
356- . get ( msg )
357- ?. get ( attr ?? '' )
358- ?. format ( scope , onError ) ;
367+ const mf = res . get ( msg ) ! . get ( attr ?? '' ) ;
368+ const str = mf ! . format ( scope , onError ) ;
359369 if ( exp instanceof RegExp ) expect ( str ) . toMatch ( exp ) ;
360370 else expect ( str ) . toBe ( exp ) ;
361371 if ( errors ?. length ) {
372+ //console.dir(mf?.resolvedOptions().message,{depth:null});
373+ //console.dir(onError.mock.calls, { depth: null });
362374 expect ( onError ) . toHaveBeenCalledTimes ( errors . length ) ;
363375 for ( let i = 0 ; i < errors . length ; ++ i ) {
364376 const [ err ] = onError . mock . calls [ i ] ;
365- expect ( err . message ) . toMatch ( errors [ i ] ) ;
377+ const expErr = errors [ i ] ;
378+ if ( typeof expErr === 'string' && ! expErr . startsWith ( '$' ) ) {
379+ expect ( err . type ) . toBe ( expErr ) ;
380+ } else {
381+ expect ( err . message ) . toMatch ( expErr ) ;
382+ }
366383 }
367384 } else {
368385 expect ( onError ) . not . toHaveBeenCalled ( ) ;
@@ -374,6 +391,20 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries(
374391 const template = Fluent . parse ( src , { withSpans : false } ) ;
375392 const res = resourceToFluent ( data , template ) ;
376393
394+ class FixResult extends Fluent . Transformer {
395+ // When converting to MF2, number wrappers are added
396+ visitSelectExpression ( node : Fluent . SelectExpression ) {
397+ if (
398+ node . selector . type === 'FunctionReference' &&
399+ node . selector . id . name === 'NUMBER'
400+ ) {
401+ node . selector = node . selector . arguments . positional [ 0 ] ;
402+ }
403+ return this . genericVisit ( node ) ;
404+ }
405+ }
406+ new FixResult ( ) . visit ( res ) ;
407+
377408 class FixExpected extends Fluent . Transformer {
378409 // MF2 uses first-match selection, so default variants will be last
379410 visitSelectExpression ( node : Fluent . SelectExpression ) {
@@ -456,7 +487,7 @@ describe('formatToParts', () => {
456487 const onError = jest . fn ( ) ;
457488 const sel = res . get ( 'sel' ) ?. get ( '' ) ?. formatToParts ( undefined , onError ) ;
458489 expect ( sel ) . toEqual ( [ { type : 'literal' , value : 'B' } ] ) ;
459- expect ( onError ) . toHaveBeenCalledTimes ( 2 ) ;
490+ expect ( onError ) . toHaveBeenCalledTimes ( 1 ) ;
460491 } ) ;
461492 } ) ;
462493
@@ -612,7 +643,9 @@ describe('formatToParts', () => {
612643 const onError = jest . fn ( ) ;
613644 const msg = res . get ( 'gender' ) ?. get ( '' ) ?. formatToParts ( undefined , onError ) ;
614645 expect ( msg ) . toEqual ( [ { type : 'literal' , value : 'N' } ] ) ;
615- expect ( onError ) . toHaveBeenCalledTimes ( 2 ) ;
646+ expect ( onError . mock . calls . map ( args => args [ 0 ] . type ) ) . toEqual ( [
647+ 'unresolved-var'
648+ ] ) ;
616649 } ) ;
617650
618651 test ( 'plural with match' , ( ) => {
@@ -631,8 +664,16 @@ describe('formatToParts', () => {
631664 } ) ;
632665
633666 test ( 'plural with non-plural input' , ( ) => {
634- const msg = res . get ( 'plural' ) ?. get ( '' ) ?. formatToParts ( { num : 'NaN' } ) ;
667+ const onError = jest . fn ( ) ;
668+ const msg = res
669+ . get ( 'plural' )
670+ ?. get ( '' )
671+ ?. formatToParts ( { num : 'NaN' } , onError ) ;
635672 expect ( msg ) . toEqual ( [ { type : 'literal' , value : 'Other' } ] ) ;
673+ expect ( onError . mock . calls . map ( args => args [ 0 ] . type ) ) . toEqual ( [
674+ 'bad-input' ,
675+ 'not-selectable'
676+ ] ) ;
636677 } ) ;
637678 } ) ;
638679} ) ;
@@ -709,12 +750,12 @@ describe('fluentToResourceData', () => {
709750 const res = resourceToFluent ( data ) ;
710751 expect ( Fluent . serialize ( res , { } ) ) . toBe ( source `
711752 single =
712- { $num ->
753+ { NUMBER( $num) ->
713754 [0] One Zero selector.
714755 *[other] One Other selector.
715756 }
716757 multi =
717- { $num ->
758+ { NUMBER( $num) ->
718759 [0]
719760 { $gender ->
720761 [feminine] Combine Zero multiple F selectors.
0 commit comments