Skip to content

Commit e0194bf

Browse files
committed
fix(@formatjs/icu-messageformat-parser): make plural check more lenient
1 parent 079ede0 commit e0194bf

File tree

3 files changed

+53
-111
lines changed

3 files changed

+53
-111
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"foo": "baz {var}"
2+
"foo": "baz {var}",
3+
"2": "{c, plural, one {#} other {#}}"
34
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"foo": "baz {var}"
2+
"foo": "baz {var}",
3+
"2": "{c, plural, one {#} few {#} other {#}}"
34
}
Lines changed: 49 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import {
22
isArgumentElement,
33
isDateElement,
4-
isLiteralElement,
54
isNumberElement,
65
isPluralElement,
7-
isPoundElement,
86
isSelectElement,
97
isTagElement,
108
isTimeElement,
119
MessageFormatElement,
1210
PluralElement,
1311
PluralOrSelectOption,
1412
SelectElement,
13+
TYPE,
1514
} from './types'
1615

1716
function cloneDeep<T>(obj: T): T {
@@ -101,122 +100,63 @@ export function hoistSelectors(
101100
return ast
102101
}
103102

104-
function isStructurallySamePluralOrSelect(
105-
el1: PluralElement | SelectElement,
106-
el2: PluralElement | SelectElement
107-
): boolean {
108-
const options1 = el1.options
109-
const options2 = el2.options
110-
if (Object.keys(options1).length !== Object.keys(options2).length) {
111-
return false
112-
}
113-
for (const key in options1) {
114-
if (!options2[key]) {
115-
return false
103+
/**
104+
* Collect all variables in an AST to Record<string, TYPE>
105+
* @param ast AST to collect variables from
106+
* @param vars Record of variable name to variable type
107+
*/
108+
function collectVariables(
109+
ast: MessageFormatElement[],
110+
vars: Map<string, TYPE> = new Map<string, TYPE>()
111+
): void {
112+
ast.forEach(el => {
113+
if (
114+
isArgumentElement(el) ||
115+
isDateElement(el) ||
116+
isTimeElement(el) ||
117+
isNumberElement(el)
118+
) {
119+
if (el.value in vars && vars.get(el.value) !== el.type) {
120+
throw new Error(`Variable ${el.value} has conflicting types`)
121+
}
122+
vars.set(el.value, el.type)
116123
}
117-
if (!isStructurallySame(options1[key].value, options2[key].value)) {
118-
return false
124+
125+
if (isPluralElement(el) || isSelectElement(el)) {
126+
vars.set(el.value, el.type)
127+
Object.keys(el.options).forEach(k => {
128+
collectVariables(el.options[k].value, vars)
129+
})
119130
}
120-
}
121-
return true
131+
132+
if (isTagElement(el)) {
133+
vars.set(el.value, el.type)
134+
collectVariables(el.children, vars)
135+
}
136+
})
122137
}
123138

139+
/**
140+
* Check if 2 ASTs are structurally the same. This primarily means that
141+
* they have the same variables with the same type
142+
* @param a
143+
* @param b
144+
* @returns
145+
*/
124146
export function isStructurallySame(
125147
a: MessageFormatElement[],
126148
b: MessageFormatElement[]
127149
): boolean {
128-
const aWithoutLiteral = a.filter(el => !isLiteralElement(el))
129-
const bWithoutLiteral = b.filter(el => !isLiteralElement(el))
130-
if (aWithoutLiteral.length !== bWithoutLiteral.length) {
150+
const aVars = new Map<string, TYPE>()
151+
const bVars = new Map<string, TYPE>()
152+
collectVariables(a, aVars)
153+
collectVariables(b, bVars)
154+
155+
if (aVars.size !== bVars.size) {
131156
return false
132157
}
133158

134-
const elementsMapInA = aWithoutLiteral.reduce<
135-
Record<string, MessageFormatElement>
136-
>((all, el) => {
137-
if (isPoundElement(el)) {
138-
all['#'] = el
139-
return all
140-
}
141-
all[el.value] = el
142-
return all
143-
}, {})
144-
145-
const elementsMapInB = bWithoutLiteral.reduce<
146-
Record<string, MessageFormatElement>
147-
>((all, el) => {
148-
if (isPoundElement(el)) {
149-
all['#'] = el
150-
return all
151-
}
152-
all[el.value] = el
153-
return all
154-
}, {})
155-
156-
for (const varName of Object.keys(elementsMapInA)) {
157-
const elA = elementsMapInA[varName]
158-
const elB = elementsMapInB[varName]
159-
if (!elB) {
160-
return false
161-
}
162-
163-
if (elA.type !== elB.type) {
164-
return false
165-
}
166-
167-
if (isLiteralElement(elA) || isLiteralElement(elB)) {
168-
continue
169-
}
170-
171-
if (
172-
isArgumentElement(elA) &&
173-
isArgumentElement(elB) &&
174-
elA.value !== elB.value
175-
) {
176-
return false
177-
}
178-
179-
if (isPoundElement(elA) || isPoundElement(elB)) {
180-
continue
181-
}
182-
183-
if (
184-
isDateElement(elA) ||
185-
isTimeElement(elA) ||
186-
isNumberElement(elA) ||
187-
isDateElement(elB) ||
188-
isTimeElement(elB) ||
189-
isNumberElement(elB)
190-
) {
191-
if (elA.value !== elB.value) {
192-
return false
193-
}
194-
}
195-
196-
if (
197-
isPluralElement(elA) &&
198-
isPluralElement(elB) &&
199-
!isStructurallySamePluralOrSelect(elA, elB)
200-
) {
201-
return false
202-
}
203-
204-
if (
205-
isSelectElement(elA) &&
206-
isSelectElement(elB) &&
207-
isStructurallySamePluralOrSelect(elA, elB)
208-
) {
209-
return false
210-
}
211-
212-
if (isTagElement(elA) && isTagElement(elB)) {
213-
if (elA.value !== elB.value) {
214-
return false
215-
}
216-
if (!isStructurallySame(elA.children, elB.children)) {
217-
return false
218-
}
219-
}
220-
}
221-
return true
159+
return Array.from(aVars.entries()).every(([key, type]) => {
160+
return bVars.has(key) && bVars.get(key) === type
161+
})
222162
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy