Skip to content

Commit cea861e

Browse files
authored
fix: French number parsing ambiguity (#7966)
* fix: French number parsing ambiguity * make code more readable and add script to help verify in future
1 parent 3f7c511 commit cea861e

File tree

2 files changed

+80
-5
lines changed

2 files changed

+80
-5
lines changed

packages/@internationalized/number/src/NumberParser.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,11 @@ class NumberParserImpl {
201201
}
202202
}
203203

204-
// fr-FR group character is char code 8239, but that's not a key on the french keyboard,
205-
// so allow 'period' as a group char and replace it with a space
206-
if (this.options.locale === 'fr-FR') {
207-
value = replaceAll(value, '.', String.fromCharCode(8239));
204+
// fr-FR group character is narrow non-breaking space, char code 8239 (U+202F), but that's not a key on the french keyboard,
205+
// so allow space and non-breaking space as a group char as well
206+
if (this.options.locale === 'fr-FR' && this.symbols.group) {
207+
value = replaceAll(value, ' ', this.symbols.group);
208+
value = replaceAll(value, /\u00A0/g, this.symbols.group);
208209
}
209210

210211
return value;
@@ -303,7 +304,7 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I
303304
return {minusSign, plusSign, decimal, group, literals, numeral, index};
304305
}
305306

306-
function replaceAll(str: string, find: string, replace: string) {
307+
function replaceAll(str: string, find: string | RegExp, replace: string) {
307308
if (str.replaceAll) {
308309
return str.replaceAll(find, replace);
309310
}

scripts/checkGroupSeparators.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// This script is to run over all of our supported locales and numbering systems
2+
// and check for the decimal and group separators so we can find
3+
// non-standard keyboard characters such as the French group separator, narrow non-breaking whitespace.
4+
// This way we can special case to be more permissive in NumberParser.
5+
6+
7+
const NUMBERING_SYSTEMS = ['latn', 'arab', 'hanidec', 'deva', 'beng'];
8+
let locales = [
9+
{label: 'French (France)', value: 'fr-FR'},
10+
{label: 'French (Canada)', value: 'fr-CA'},
11+
{label: 'German (Germany)', value: 'de-DE'},
12+
{label: 'English (Great Britain)', value: 'en-GB'},
13+
{label: 'English (United States)', value: 'en-US'},
14+
{label: 'Japanese (Japan)', value: 'ja-JP'},
15+
{label: 'Danish (Denmark)', value: 'da-DK'},
16+
{label: 'Dutch (Netherlands)', value: 'nl-NL'},
17+
{label: 'Finnish (Finland)', value: 'fi-FI'},
18+
{label: 'Italian (Italy)', value: 'it-IT'},
19+
{label: 'Norwegian (Norway)', value: 'nb-NO'},
20+
{label: 'Spanish (Spain)', value: 'es-ES'},
21+
{label: 'Swedish (Sweden)', value: 'sv-SE'},
22+
{label: 'Portuguese (Brazil)', value: 'pt-BR'},
23+
{label: 'Chinese (Simplified)', value: 'zh-CN'},
24+
{label: 'Chinese (Traditional)', value: 'zh-TW'},
25+
{label: 'Korean (Korea)', value: 'ko-KR'},
26+
{label: 'Bulgarian (Bulgaria)', value: 'bg-BG'},
27+
{label: 'Croatian (Croatia)', value: 'hr-HR'},
28+
{label: 'Czech (Czech Republic)', value: 'cs-CZ'},
29+
{label: 'Estonian (Estonia)', value: 'et-EE'},
30+
{label: 'Hungarian (Hungary)', value: 'hu-HU'},
31+
{label: 'Latvian (Latvia)', value: 'lv-LV'},
32+
{label: 'Lithuanian (Lithuania)', value: 'lt-LT'},
33+
{label: 'Polish (Poland)', value: 'pl-PL'},
34+
{label: 'Romanian (Romania)', value: 'ro-RO'},
35+
{label: 'Russian (Russia)', value: 'ru-RU'},
36+
{label: 'Serbian (Serbia)', value: 'sr-SP'},
37+
{label: 'Slovakian (Slovakia)', value: 'sk-SK'},
38+
{label: 'Slovenian (Slovenia)', value: 'sl-SI'},
39+
{label: 'Turkish (Turkey)', value: 'tr-TR'},
40+
{label: 'Ukrainian (Ukraine)', value: 'uk-UA'},
41+
{label: 'Arabic (United Arab Emirates)', value: 'ar-AE'}, // ar-SA??
42+
{label: 'Greek (Greece)', value: 'el-GR'},
43+
{label: 'Hebrew (Israel)', value: 'he-IL'}
44+
];
45+
46+
let separators = new Map();
47+
for (let nums of NUMBERING_SYSTEMS) {
48+
for (let locale of locales) {
49+
let formatter = new Intl.NumberFormat(locale.value + '-u-nu-' + nums, {});
50+
let parts = formatter.formatToParts(10000000000);
51+
let separator = parts.find(p => p.type === 'group')?.value;
52+
if (separators.has(separator)) {
53+
separators.set(separator, [...separators.get(separator), locale.value + '-u-nu-' + nums])
54+
} else {
55+
separators.set(separator, [separator.charCodeAt(0), locale.value + '-u-nu-' + nums]);
56+
}
57+
}
58+
}
59+
console.log(separators);
60+
61+
let decimals = new Map();
62+
for (let nums of NUMBERING_SYSTEMS) {
63+
for (let locale of locales) {
64+
let formatter = new Intl.NumberFormat(locale.value + '-u-nu-' + nums, {minimumFractionDigits: 2});
65+
let parts = formatter.formatToParts(10000.0000001);
66+
let decimal = parts.find(p => p.type === 'decimal')?.value;
67+
if (decimals.has(decimal)) {
68+
decimals.set(decimal, [...decimals.get(decimal), locale.value + '-u-nu-' + nums])
69+
} else {
70+
decimals.set(decimal, [decimal.charCodeAt(0), locale.value + '-u-nu-' + nums]);
71+
}
72+
}
73+
}
74+
console.log(decimals);

0 commit comments

Comments
 (0)