Skip to content

Commit 518d7cf

Browse files
author
Long Ho
committed
fix(@formatjs/intl-localematcher): add weight to requested locale order, fix #4258
1 parent 18b0a3b commit 518d7cf

5 files changed

Lines changed: 190 additions & 29 deletions

File tree

packages/intl-localematcher/abstract/BestFitMatcher.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,26 @@ export function BestFitMatcher(
1414
): LookupMatcherResult {
1515
let foundLocale: string | undefined
1616
let extension: string | undefined
17-
for (const l of requestedLocales) {
18-
const matchedLocale = findBestMatch(
19-
l.replace(UNICODE_EXTENSION_SEQUENCE_REGEX, ''),
20-
availableLocales
21-
)
22-
if (matchedLocale) {
23-
foundLocale = matchedLocale
17+
const noExtensionLocales: string[] = []
18+
const noExtensionLocaleMap = requestedLocales.reduce<Record<string, string>>(
19+
(all, l) => {
2420
const noExtensionLocale = l.replace(UNICODE_EXTENSION_SEQUENCE_REGEX, '')
25-
extension = l.slice(noExtensionLocale.length, l.length) || undefined
26-
break
27-
}
21+
noExtensionLocales.push(noExtensionLocale)
22+
all[noExtensionLocale] = l
23+
return all
24+
},
25+
{}
26+
)
27+
28+
const result = findBestMatch(noExtensionLocales, availableLocales)
29+
if (result.matchedSupportedLocale && result.matchedDesiredLocale) {
30+
foundLocale = result.matchedSupportedLocale
31+
extension =
32+
noExtensionLocaleMap[result.matchedDesiredLocale].slice(
33+
result.matchedDesiredLocale.length
34+
) || undefined
2835
}
36+
2937
if (!foundLocale) {
3038
return {locale: getDefaultLocale()}
3139
}

packages/intl-localematcher/abstract/utils.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -205,25 +205,44 @@ export function findMatchingDistance(desired: string, supported: string) {
205205
return matchingDistance
206206
}
207207

208+
interface LocaleMatchingResult {
209+
distances: Record<string, Record<string, number>>
210+
matchedSupportedLocale?: string
211+
matchedDesiredLocale?: string
212+
}
213+
208214
export function findBestMatch(
209-
desired: string,
215+
requestedLocales: readonly string[],
210216
supportedLocales: readonly string[],
211217
threshold = DEFAULT_MATCHING_THRESHOLD
212-
): string | undefined {
213-
let bestMatch = undefined
218+
): LocaleMatchingResult {
214219
let lowestDistance = Infinity
215-
supportedLocales.forEach((supported, i) => {
216-
// Add some weight to the distance based on the order of the supported locales
217-
const distance = findMatchingDistance(desired, supported) + i
218-
if (distance < lowestDistance) {
219-
lowestDistance = distance
220-
bestMatch = supported
220+
let result: LocaleMatchingResult = {
221+
matchedDesiredLocale: '',
222+
distances: {},
223+
}
224+
requestedLocales.forEach((desired, i) => {
225+
if (!result.distances[desired]) {
226+
result.distances[desired] = {}
221227
}
228+
supportedLocales.forEach((supported, j) => {
229+
// Add some weight to the distance based on the order of the supported locales
230+
// Add penalty for the order of the requested locales
231+
const distance = findMatchingDistance(desired, supported) + j + i * 40
232+
233+
result.distances[desired][supported] = distance
234+
if (distance < lowestDistance) {
235+
lowestDistance = distance
236+
result.matchedDesiredLocale = desired
237+
result.matchedSupportedLocale = supported
238+
}
239+
})
222240
})
223241

224242
if (lowestDistance >= threshold) {
225-
return
243+
result.matchedDesiredLocale = undefined
244+
result.matchedSupportedLocale = undefined
226245
}
227246

228-
return bestMatch
247+
return result
229248
}

packages/intl-localematcher/tests/BestFitMatcher.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ test('BestFitMatcher extension', function () {
2424
extension: '-u-ca-gregory',
2525
})
2626
})
27+
28+
test('GH #4258', function () {
29+
expect(
30+
BestFitMatcher(['en', 'en-US', 'fr-FR'], ['de-DE', 'fr'], () => 'en-US')
31+
).toEqual({
32+
locale: 'fr-FR',
33+
})
34+
})
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`findBestMatch 1`] = `
4+
{
5+
"distances": {
6+
"en-US": {
7+
"en-US": 0,
8+
},
9+
},
10+
"matchedDesiredLocale": "en-US",
11+
"matchedSupportedLocale": "en-US",
12+
}
13+
`;
14+
15+
exports[`findBestMatch 2`] = `
16+
{
17+
"distances": {
18+
"sr-Latn-BA": {
19+
"bs": 800,
20+
"sh": 41,
21+
},
22+
},
23+
"matchedDesiredLocale": "sr-Latn-BA",
24+
"matchedSupportedLocale": "sh",
25+
}
26+
`;
27+
28+
exports[`findBestMatch 3`] = `
29+
{
30+
"distances": {
31+
"fr-XX": {
32+
"en": 839,
33+
"fr": 40,
34+
},
35+
},
36+
"matchedDesiredLocale": "fr-XX",
37+
"matchedSupportedLocale": "fr",
38+
}
39+
`;
40+
41+
exports[`findBestMatch 4`] = `
42+
{
43+
"distances": {
44+
"zh-TW": {
45+
"zh": 540,
46+
"zh-Hant": 1,
47+
},
48+
},
49+
"matchedDesiredLocale": "zh-TW",
50+
"matchedSupportedLocale": "zh-Hant",
51+
}
52+
`;
53+
54+
exports[`findBestMatch 5`] = `
55+
{
56+
"distances": {
57+
"th-u-ca-gregory": {
58+
"th": 0,
59+
},
60+
},
61+
"matchedDesiredLocale": "th-u-ca-gregory",
62+
"matchedSupportedLocale": "th",
63+
}
64+
`;
65+
66+
exports[`findBestMatch 6`] = `
67+
{
68+
"distances": {
69+
"es-co": {
70+
"en": 839,
71+
"es-419": 40,
72+
},
73+
},
74+
"matchedDesiredLocale": "es-co",
75+
"matchedSupportedLocale": "es-419",
76+
}
77+
`;
78+
79+
exports[`findBestMatch 7`] = `
80+
{
81+
"distances": {
82+
"es-co": {
83+
"en": 839,
84+
"es": 50,
85+
"es-419": 41,
86+
},
87+
},
88+
"matchedDesiredLocale": "es-co",
89+
"matchedSupportedLocale": "es-419",
90+
}
91+
`;
92+
93+
exports[`findBestMatch 8`] = `
94+
{
95+
"distances": {
96+
"pt-mz": {
97+
"pt-BR": 50,
98+
"pt-PT": 39,
99+
},
100+
},
101+
"matchedDesiredLocale": "pt-mz",
102+
"matchedSupportedLocale": "pt-PT",
103+
}
104+
`;
105+
106+
exports[`findBestMatch 9`] = `
107+
{
108+
"distances": {
109+
"de-DE": {
110+
"en": 838,
111+
"en-US": 839,
112+
"fr-FR": 842,
113+
},
114+
"fr-FR": {
115+
"en": 878,
116+
"en-US": 879,
117+
"fr-FR": 42,
118+
},
119+
},
120+
"matchedDesiredLocale": "fr-FR",
121+
"matchedSupportedLocale": "fr-FR",
122+
}
123+
`;

packages/intl-localematcher/tests/utils.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ test('findMatchingDistance', () => {
1111
expect(findMatchingDistance('sr-Latn-BA', 'bs-Latn-BA')).toBe(800)
1212
})
1313
test('findBestMatch', () => {
14-
expect(findBestMatch('en-US', ['en-US'])).toBe('en-US')
15-
expect(findBestMatch('sr-Latn-BA', ['bs', 'sh'])).toBe('sh')
16-
expect(findBestMatch('fr-XX', ['fr', 'en'])).toBe('fr')
17-
expect(findBestMatch('zh-TW', ['zh', 'zh-Hant'])).toBe('zh-Hant')
18-
expect(findBestMatch('th-u-ca-gregory', ['th'])).toBe('th')
14+
expect(findBestMatch(['en-US'], ['en-US'])).toMatchSnapshot()
15+
expect(findBestMatch(['sr-Latn-BA'], ['bs', 'sh'])).toMatchSnapshot()
16+
expect(findBestMatch(['fr-XX'], ['fr', 'en'])).toMatchSnapshot()
17+
expect(findBestMatch(['zh-TW'], ['zh', 'zh-Hant'])).toMatchSnapshot()
18+
expect(findBestMatch(['th-u-ca-gregory'], ['th'])).toMatchSnapshot()
1919

20-
expect(findBestMatch('es-co', ['en', 'es-419'])).toBe('es-419')
21-
expect(findBestMatch('es-co', ['en', 'es', 'es-419'])).toBe('es-419')
22-
expect(findBestMatch('pt-mz', ['pt-PT', 'pt-BR'])).toBe('pt-PT')
20+
expect(findBestMatch(['es-co'], ['en', 'es-419'])).toMatchSnapshot()
21+
expect(findBestMatch(['es-co'], ['en', 'es', 'es-419'])).toMatchSnapshot()
22+
expect(findBestMatch(['pt-mz'], ['pt-PT', 'pt-BR'])).toMatchSnapshot()
23+
expect(
24+
findBestMatch(['de-DE', 'fr-FR'], ['en', 'en-US', 'fr-FR'])
25+
).toMatchSnapshot()
2326
})

0 commit comments

Comments
 (0)