Skip to content

Commit ec008b2

Browse files
authored
Improve Language Negotiation logic to match the C++ implementation (#19)
1 parent fee7f4b commit ec008b2

File tree

9 files changed

+249
-312
lines changed

9 files changed

+249
-312
lines changed

fluent-langneg/src/index.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function GetOption(options, property, type, values, fallback) {
1616
if (type === 'boolean') {
1717
value = new Boolean(value);
1818
} else if (type === 'string') {
19-
value = value.toString();
19+
value = String(value);
2020
}
2121

2222
if (values !== undefined && values.indexOf(value) === -1) {
@@ -90,22 +90,27 @@ export default function negotiateLanguages(
9090
const strategy = GetOption(options, 'strategy', 'string',
9191
['filtering', 'matching', 'lookup'], 'filtering');
9292

93-
if (strategy === 'lookup' && defaultLocale === undefined) {
93+
if (strategy === 'lookup' && !defaultLocale) {
9494
throw new Error('defaultLocale cannot be undefined for strategy `lookup`');
9595
}
9696

97+
const resolvedReqLoc = Array.from(Object(requestedLocales)).map(loc => {
98+
return String(loc);
99+
});
100+
const resolvedAvailLoc = Array.from(Object(availableLocales)).map(loc => {
101+
return String(loc);
102+
});
103+
97104
const supportedLocales = filterMatches(
98-
requestedLocales, availableLocales, strategy, likelySubtags
105+
resolvedReqLoc,
106+
resolvedAvailLoc, strategy, likelySubtags
99107
);
100108

101109
if (strategy === 'lookup') {
102110
if (supportedLocales.length === 0) {
103111
supportedLocales.push(defaultLocale);
104112
}
105-
return supportedLocales;
106-
}
107-
108-
if (defaultLocale && !supportedLocales.includes(defaultLocale)) {
113+
} else if (defaultLocale && !supportedLocales.includes(defaultLocale)) {
109114
supportedLocales.push(defaultLocale);
110115
}
111116
return supportedLocales;

fluent-langneg/src/locale.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint no-magic-numbers: 0 */
22

3+
import { getLikelySubtagsMin } from './subtags';
4+
35
const languageCodeRe = '([a-z]{2,3}|\\*)';
46
const scriptCodeRe = '(?:-([a-z]{4}|\\*))';
57
const regionCodeRe = '(?:-([a-z]{2}|\\*))';
6-
const variantCodeRe = '(?:-([a-z]+|\\*))';
8+
const variantCodeRe = '(?:-([a-z]{3}|\\*))';
79

810
/**
911
* Regular expression splitting locale id into four pieces:
@@ -33,7 +35,7 @@ export default class Locale {
3335
* properly parsed as `en-*-US-*`.
3436
*/
3537
constructor(locale, range = false) {
36-
const result = localeRe.exec(locale);
38+
const result = localeRe.exec(locale.replace(/_/g, '-'));
3739
if (!result) {
3840
return;
3941
}
@@ -49,6 +51,7 @@ export default class Locale {
4951
this.script = script;
5052
this.region = region;
5153
this.variant = variant;
54+
this.string = locale;
5255
}
5356

5457
isEqual(locale) {
@@ -63,4 +66,23 @@ export default class Locale {
6366
this[part].toLowerCase() === locale[part].toLowerCase());
6467
});
6568
}
69+
70+
setVariantRange() {
71+
this.variant = '*';
72+
}
73+
74+
setRegionRange() {
75+
this.region = '*';
76+
}
77+
78+
addLikelySubtags() {
79+
const newLocale = getLikelySubtagsMin(this.string.toLowerCase());
80+
81+
if (newLocale) {
82+
localeParts.forEach(part => this[part] = newLocale[part]);
83+
this.string = newLocale.string;
84+
return true;
85+
}
86+
return false;
87+
}
6688
}

fluent-langneg/src/matches.js

Lines changed: 61 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,8 @@
11
/* eslint no-magic-numbers: 0 */
2-
/* eslint complexity: ["error", { "max": 22 }] */
2+
/* eslint complexity: ["error", { "max": 27 }] */
33

44
import Locale from './locale';
5-
import { getLikelySubtagsMin } from './subtags';
65

7-
/**
8-
* Attempts to build a most likely maximum version of the locale id based
9-
* on it's minimal version.
10-
*
11-
* If possible it uses the lookup table above. If this one is missing,
12-
* it only attempts to place the language code in the region position.
13-
*/
14-
function getLikelySubtagsLocale(loc, likelySubtags) {
15-
if (likelySubtags) {
16-
for (const key of Object.keys(likelySubtags)) {
17-
if (key.toLowerCase() === loc.toLowerCase()) {
18-
return new Locale(likelySubtags[key]);
19-
}
20-
}
21-
return null;
22-
}
23-
return getLikelySubtagsMin(loc);
24-
}
25-
26-
/**
27-
* Replaces the region position with a range to allow for matches
28-
* with the same language/script but from different region.
29-
*/
30-
function regionRangeFor(locale) {
31-
locale.region = '*';
32-
return locale;
33-
}
34-
35-
function variantRangeFor(locale) {
36-
locale.variant = '*';
37-
return locale;
38-
}
39-
40-
function getOrParseLocaleRange(localeStr, cache) {
41-
if (!cache.has(localeStr)) {
42-
cache.set(localeStr, new Locale(localeStr, true));
43-
}
44-
return cache.get(localeStr);
45-
}
466
/**
477
* Negotiates the languages between the list of requested locales against
488
* a list of available locales.
@@ -114,43 +74,57 @@ function getOrParseLocaleRange(localeStr, cache) {
11474
* against `sr-Latn`.
11575
*/
11676
export default function filterMatches(
117-
requestedLocales, availableLocales, strategy, likelySubtags
77+
requestedLocales, availableLocales, strategy
11878
) {
11979
const supportedLocales = new Set();
12080

121-
const availableLocalesCache = new Map();
81+
const availLocales =
82+
new Set(availableLocales.map(locale => new Locale(locale, true)));
12283

12384
outer:
12485
for (const reqLocStr of requestedLocales) {
125-
if (strategy === 'lookup' && supportedLocales.size > 0) {
126-
return Array.from(supportedLocales);
127-
}
128-
12986
const reqLocStrLC = reqLocStr.toLowerCase();
87+
const requestedLocale = new Locale(reqLocStrLC);
88+
89+
if (requestedLocale.language === undefined) {
90+
continue;
91+
}
13092

13193
// Attempt to make an exact match
13294
// Example: `en-US` === `en-US`
13395
for (const availableLocale of availableLocales) {
13496
if (reqLocStrLC === availableLocale.toLowerCase()) {
13597
supportedLocales.add(availableLocale);
136-
if (strategy !== 'matching') {
98+
for (const loc of availLocales) {
99+
if (loc.isEqual(requestedLocale)) {
100+
availLocales.delete(loc);
101+
break;
102+
}
103+
}
104+
if (strategy === 'lookup') {
105+
return Array.from(supportedLocales);
106+
} else if (strategy === 'matching') {
137107
continue outer;
108+
} else {
109+
break;
138110
}
139111
}
140112
}
141113

142-
const requestedLocale = new Locale(reqLocStrLC);
143114

144115
// Attempt to match against the available range
145116
// This turns `en` into `en-*-*-*` and `en-US` into `en-*-US-*`
146117
// Example: ['en-US'] * ['en'] = ['en']
147-
for (const availableLocale of availableLocales) {
148-
if (requestedLocale.matches(
149-
getOrParseLocaleRange(availableLocale, availableLocalesCache)
150-
)) {
151-
supportedLocales.add(availableLocale);
152-
if (strategy !== 'matching') {
118+
for (const availableLocale of availLocales) {
119+
if (requestedLocale.matches(availableLocale)) {
120+
supportedLocales.add(availableLocale.string);
121+
availLocales.delete(availableLocale);
122+
if (strategy === 'lookup') {
123+
return Array.from(supportedLocales);
124+
} else if (strategy === 'matching') {
153125
continue outer;
126+
} else {
127+
break;
154128
}
155129
}
156130
}
@@ -159,48 +133,54 @@ export default function filterMatches(
159133
// If data is available, it'll expand `en` into `en-Latn-US` and
160134
// `zh` into `zh-Hans-CN`.
161135
// Example: ['en'] * ['en-GB', 'en-US'] = ['en-US']
162-
const maxRequestedLocale =
163-
getLikelySubtagsLocale(reqLocStrLC, likelySubtags);
164-
165-
if (maxRequestedLocale) {
166-
for (const availableLocale of availableLocales) {
167-
if (maxRequestedLocale.matches(
168-
getOrParseLocaleRange(availableLocale, availableLocalesCache)
169-
)) {
170-
supportedLocales.add(availableLocale);
171-
if (strategy !== 'matching') {
136+
if (requestedLocale.addLikelySubtags()) {
137+
for (const availableLocale of availLocales) {
138+
if (requestedLocale.matches(availableLocale)) {
139+
supportedLocales.add(availableLocale.string);
140+
availLocales.delete(availableLocale);
141+
if (strategy === 'lookup') {
142+
return Array.from(supportedLocales);
143+
} else if (strategy === 'matching') {
172144
continue outer;
145+
} else {
146+
break;
173147
}
174148
}
175149
}
176150
}
177151

178152
// Attempt to look up for a different variant for the same locale ID
179153
// Example: ['en-US-mac'] * ['en-US-win'] = ['en-US-win']
180-
const variantRange = variantRangeFor(maxRequestedLocale || requestedLocale);
181-
182-
for (const availableLocale of availableLocales) {
183-
if (variantRange.matches(
184-
getOrParseLocaleRange(availableLocale, availableLocalesCache)
185-
)) {
186-
supportedLocales.add(availableLocale);
187-
if (strategy !== 'matching') {
154+
requestedLocale.setVariantRange();
155+
156+
for (const availableLocale of availLocales) {
157+
if (requestedLocale.matches(availableLocale)) {
158+
supportedLocales.add(availableLocale.string);
159+
availLocales.delete(availableLocale);
160+
if (strategy === 'lookup') {
161+
return Array.from(supportedLocales);
162+
} else if (strategy === 'matching') {
188163
continue outer;
164+
} else {
165+
break;
189166
}
190167
}
191168
}
192169

193170
// Attempt to look up for a different region for the same locale ID
194171
// Example: ['en-US'] * ['en-AU'] = ['en-AU']
195-
const regionRange = regionRangeFor(variantRange);
196-
197-
for (const availableLocale of availableLocales) {
198-
if (regionRange.matches(
199-
getOrParseLocaleRange(availableLocale, availableLocalesCache)
200-
)) {
201-
supportedLocales.add(availableLocale);
202-
if (strategy !== 'matching') {
172+
requestedLocale.setRegionRange();
173+
174+
for (const availableLocale of availLocales) {
175+
if (requestedLocale.matches(availableLocale)) {
176+
supportedLocales.add(availableLocale.string);
177+
availLocales.delete(availableLocale);
178+
if (strategy === 'lookup') {
179+
return Array.from(supportedLocales);
180+
} else if (strategy === 'matching') {
203181
continue outer;
182+
} else {
183+
break;
204184
}
205185
}
206186
}

fluent-langneg/src/subtags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function getLikelySubtagsMin(loc) {
5656
const locale = new Locale(loc);
5757
if (regionMatchingLangs.includes(locale.language)) {
5858
locale.region = locale.language;
59+
locale.string = `${locale.language}-${locale.region}`;
5960
return locale;
6061
}
6162
return null;

0 commit comments

Comments
 (0)