From f5d1bba7b33b697eeb73bd9de1c01320f3d43bab Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 14 Mar 2023 13:30:15 +0200 Subject: [PATCH 1/2] feat(mf2): Use column-first rather than first-match selection --- packages/mf2-icu-mf1/src/runtime.ts | 10 ++-- .../src/__fixtures/test-messages.json | 2 +- .../src/message-value/message-fallback.ts | 4 +- .../src/message-value/message-markup.ts | 8 +-- .../src/message-value/message-number.ts | 9 +-- .../src/message-value/message-value.ts | 8 ++- .../src/message-value/resolved-message.ts | 60 ++++++++++++++----- 7 files changed, 69 insertions(+), 32 deletions(-) diff --git a/packages/mf2-icu-mf1/src/runtime.ts b/packages/mf2-icu-mf1/src/runtime.ts index 4e01ca46..38ef9784 100644 --- a/packages/mf2-icu-mf1/src/runtime.ts +++ b/packages/mf2-icu-mf1/src/runtime.ts @@ -104,17 +104,17 @@ class MessageMF1Number extends MessageNumber { super(locale, number, opt); } - matchSelectKey(key: string) { + selectKey(keys: Set) { let num = this.value; const offset = getMF1Offset(this.options); if (offset) { if (typeof num === 'bigint') num += BigInt(offset); else num += offset; } - return ( - (/^[0-9]+$/.test(key) && key === String(num)) || - key === this.getPluralCategory() - ); + const str = String(num); + if (keys.has(str)) return str; + const cat = this.getPluralCategory(); + return keys.has(cat) ? cat : null; } } diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index aa858396..3191f862 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -123,7 +123,7 @@ { "src": "match {$foo} when one {one} when 1 {=1} when * {other}", "params": { "foo": 1 }, - "exp": "one" + "exp": "=1" }, { "src": "match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}", diff --git a/packages/mf2-messageformat/src/message-value/message-fallback.ts b/packages/mf2-messageformat/src/message-value/message-fallback.ts index a5d20cf5..c831bf59 100644 --- a/packages/mf2-messageformat/src/message-value/message-fallback.ts +++ b/packages/mf2-messageformat/src/message-value/message-fallback.ts @@ -19,8 +19,8 @@ export class MessageFallback extends MessageValue { super(FALLBACK, locale, undefined, fmt); } - matchSelectKey() { - return false; + selectKey() { + return null; } toString() { diff --git a/packages/mf2-messageformat/src/message-value/message-markup.ts b/packages/mf2-messageformat/src/message-value/message-markup.ts index 6f26d183..3c54f126 100644 --- a/packages/mf2-messageformat/src/message-value/message-markup.ts +++ b/packages/mf2-messageformat/src/message-value/message-markup.ts @@ -29,8 +29,8 @@ export class MessageMarkupStart extends MessageValue { this.options = { ...options }; } - matchSelectKey() { - return false; + selectKey() { + return null; } toString() { @@ -52,8 +52,8 @@ export class MessageMarkupEnd extends MessageValue { super(MARKUP_END, locale, name, options); } - matchSelectKey() { - return false; + selectKey() { + return null; } toString() { diff --git a/packages/mf2-messageformat/src/message-value/message-number.ts b/packages/mf2-messageformat/src/message-value/message-number.ts index 9c213063..19d3f915 100644 --- a/packages/mf2-messageformat/src/message-value/message-number.ts +++ b/packages/mf2-messageformat/src/message-value/message-number.ts @@ -66,10 +66,11 @@ export class MessageNumber extends MessageValue { * For example, cardinal English plurals only use `one` and `other`, * so a key `zero` will never be matched for that locale. */ - matchSelectKey(key: string) { - if (/^[0-9]+$/.test(key) && key === String(this.value)) return true; - if (key === this.getPluralCategory()) return { plural: key }; - return false; + selectKey(keys: Set) { + const str = String(this.value); + if (keys.has(str)) return str; + const cat = this.getPluralCategory(); + return keys.has(cat) ? cat : null; } toParts(): Intl.NumberFormatPart[] { diff --git a/packages/mf2-messageformat/src/message-value/message-value.ts b/packages/mf2-messageformat/src/message-value/message-value.ts index 0712d077..c9db6301 100644 --- a/packages/mf2-messageformat/src/message-value/message-value.ts +++ b/packages/mf2-messageformat/src/message-value/message-value.ts @@ -79,8 +79,12 @@ export class MessageValue { this.#localeContext = locale; } - matchSelectKey(key: string): boolean | Meta { - return this.value != null && String(this.value) === key; + selectKey(keys: Set): string | null { + if (this.value != null) { + const str = String(this.value); + if (keys.has(str)) return str; + } + return null; } toString(onError?: Context['onError']): string { diff --git a/packages/mf2-messageformat/src/message-value/resolved-message.ts b/packages/mf2-messageformat/src/message-value/resolved-message.ts index e9d5a4d7..f709e223 100644 --- a/packages/mf2-messageformat/src/message-value/resolved-message.ts +++ b/packages/mf2-messageformat/src/message-value/resolved-message.ts @@ -14,22 +14,52 @@ function getPattern( return { pattern: message.pattern.body }; case 'select': { - const resSelectors = message.selectors.map(sel => context.resolve(sel)); + const ctx = message.selectors.map(sel => ({ + selector: context.resolve(sel), + best: null as string | null, + keys: null as Set | null + })); - cases: for (const { keys, value } of message.variants) { - let meta: Meta | undefined = undefined; - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const rs = resSelectors[i]; - const match = key.type === '*' || rs?.matchSelectKey(key.value); - if (!match) continue cases; - if (typeof match === 'object') - meta = Object.assign(meta ?? {}, match); + let candidates = message.variants; + loop: for (let i = 0; i < ctx.length; ++i) { + const sc = ctx[i]; + if (!sc.keys) { + sc.keys = new Set(); + for (const { keys } of candidates) { + const key = keys[i]; + if (!key) break loop; // key-mismatch error + if (key.type !== '*') sc.keys.add(key.value); + } + } + sc.best = sc.keys.size ? sc.selector.selectKey(sc.keys) : null; + + // Leave out all candidate variants that aren't the best, + // or only the catchall ones, if nothing else matches. + candidates = candidates.filter(v => { + const k = v.keys[i]; + if (k.type === '*') return sc.best == null; + return sc.best === k.value; + }); + + // If we've run out of candidates, + // drop the previous best key of the preceding selector, + // reset all subsequent key sets, + // and restart the loop. + if (candidates.length === 0) { + if (i === 0) break; // No match; should not happen + const prev = ctx[i - 1]; + if (prev.best == null) prev.keys?.clear(); + else prev.keys?.delete(prev.best); + for (let j = i; j < ctx.length; ++j) ctx[j].keys = null; + candidates = message.variants; + i = -1; } - return { pattern: value.body, meta }; } - return { pattern: [], meta: { selectResult: 'no-match' } }; + const res = candidates[0]; + return res + ? { pattern: res.value.body } + : { pattern: [], meta: { selectResult: 'no-match' } }; } default: @@ -52,8 +82,10 @@ export class ResolvedMessage extends MessageValue { super(MESSAGE, context, resMsg, { meta, source }); } - matchSelectKey(key: string) { - return this.toString() === key; + selectKey(keys: Set) { + let hasError = false; + const str = this.toString(() => (hasError = true)); + return !hasError && keys.has(str) ? str : null; } toString(onError?: Context['onError']) { From 102121b0bb765f8d11ca4035331bf6ba6585c19d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 14 Mar 2023 15:03:26 +0200 Subject: [PATCH 2/2] chore: Publish - messageformat@4.0.0-3.cf --- package-lock.json | 2 +- packages/mf2-messageformat/CHANGELOG.md | 4 ++++ packages/mf2-messageformat/package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a79784ca..0e274f03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12322,7 +12322,7 @@ }, "packages/mf2-messageformat": { "name": "messageformat", - "version": "4.0.0-3", + "version": "4.0.0-3.cf", "license": "Apache-2.0" }, "packages/mf2-xliff": { diff --git a/packages/mf2-messageformat/CHANGELOG.md b/packages/mf2-messageformat/CHANGELOG.md index 2ad9f1f5..d4b6318f 100644 --- a/packages/mf2-messageformat/CHANGELOG.md +++ b/packages/mf2-messageformat/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [4.0.0-3.cf](https://github.com/messageformat/messageformat/compare/messageformat@4.0.0-3...messageformat@4.0.0-3.cf) (2023-03-14) + +* Use column-first rather than first-match selection ([f5d1bba](https://github.com/messageformat/messageformat/commit/f5d1bba7b33b697eeb73bd9de1c01320f3d43bab)) + ## [4.0.0-3](https://github.com/messageformat/messageformat/compare/messageformat@4.0.0-2...messageformat@4.0.0-3) (2023-03-14) * Use `|` rather than `()` as literal quotes ([15e1fcd](https://github.com/messageformat/messageformat/commit/15e1fcd65341a5ab536a06d4401b7f488b8cdfcc)) diff --git a/packages/mf2-messageformat/package.json b/packages/mf2-messageformat/package.json index 809063ab..8f79c0b0 100644 --- a/packages/mf2-messageformat/package.json +++ b/packages/mf2-messageformat/package.json @@ -1,6 +1,6 @@ { "name": "messageformat", - "version": "4.0.0-3", + "version": "4.0.0-3.cf", "description": "Intl.MessageFormat / Unicode MessageFormat 2 parser, runtime and polyfill", "keywords": [ "messageformat",