1'use strict';
2const common = require('../common');
3const assert = require('assert');
4const { execFileSync } = require('child_process');
5
6// system-icu should not be tested
7const hasBuiltinICU = process.config.variables.icu_gyp_path === 'tools/icu/icu-generic.gyp';
8if (!hasBuiltinICU)
9  common.skip('system ICU');
10
11// small-icu doesn't support non-English locales
12const hasFullICU = (() => {
13  try {
14    const january = new Date(9e8);
15    const spanish = new Intl.DateTimeFormat('es', { month: 'long' });
16    return spanish.format(january) === 'enero';
17  } catch {
18    return false;
19  }
20})();
21if (!hasFullICU)
22  common.skip('small ICU');
23
24const icuVersionMajor = Number(process.config.variables.icu_ver_major ?? 0);
25if (icuVersionMajor < 71)
26  common.skip('ICU too old');
27
28
29function runEnvOutside(addEnv, code, ...args) {
30  return execFileSync(
31    process.execPath,
32    ['-e', `process.stdout.write(String(${code}));`],
33    { env: { ...process.env, ...addEnv }, encoding: 'utf8' }
34  );
35}
36
37function runEnvInside(addEnv, func, ...args) {
38  Object.assign(process.env, addEnv); // side effects!
39  return func(...args);
40}
41
42function isPack(array) {
43  const firstItem = array[0];
44  return array.every((item) => item === firstItem);
45}
46
47function isSet(array) {
48  const deduped = new Set(array);
49  return array.length === deduped.size;
50}
51
52
53const localesISO639 = [
54  'eng', 'cmn', 'hin', 'spa',
55  'fra', 'arb', 'ben', 'rus',
56  'por', 'urd', 'ind', 'deu',
57  'jpn', 'pcm', 'mar', 'tel',
58];
59
60const locales = [
61  'en', 'zh', 'hi', 'es',
62  'fr', 'ar', 'bn', 'ru',
63  'pt', 'ur', 'id', 'de',
64  'ja', 'pcm', 'mr', 'te',
65];
66
67// These must not overlap
68const zones = [
69  'America/New_York',
70  'UTC',
71  'Asia/Irkutsk',
72  'Australia/North',
73  'Antarctica/South_Pole',
74];
75
76
77assert.deepStrictEqual(Intl.getCanonicalLocales(localesISO639), locales);
78
79
80// On some platforms these keep original locale (for example, 'January')
81const enero = runEnvOutside(
82  { LANG: 'es' },
83  'new Intl.DateTimeFormat(undefined, { month: "long" } ).format(new Date(9e8))'
84);
85const janvier = runEnvOutside(
86  { LANG: 'fr' },
87  'new Intl.DateTimeFormat(undefined, { month: "long" } ).format(new Date(9e8))'
88);
89const isMockable = enero !== janvier;
90
91// Tests with mocked env
92if (isMockable) {
93  assert.strictEqual(
94    isSet(zones.map((TZ) => runEnvOutside({ TZ }, 'new Date(333333333333).toString()'))),
95    true
96  );
97  assert.strictEqual(
98    isSet(zones.map((TZ) => runEnvOutside({ TZ }, 'new Date(333333333333).toLocaleString()'))),
99    true
100  );
101  assert.deepStrictEqual(
102    locales.map((LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Date(333333333333).toString()')),
103    [
104      'Fri Jul 25 1980 01:35:33 GMT+0100 (Central European Standard Time)',
105      'Fri Jul 25 1980 01:35:33 GMT+0100 (中欧标准时间)',
106      'Fri Jul 25 1980 01:35:33 GMT+0100 (मध्य यूरोपीय मानक समय)',
107      'Fri Jul 25 1980 01:35:33 GMT+0100 (hora estándar de Europa central)',
108      'Fri Jul 25 1980 01:35:33 GMT+0100 (heure normale d’Europe centrale)',
109      'Fri Jul 25 1980 01:35:33 GMT+0100 (توقيت وسط أوروبا الرسمي)',
110      'Fri Jul 25 1980 01:35:33 GMT+0100 (মধ্য ইউরোপীয় মানক সময়)',
111      'Fri Jul 25 1980 01:35:33 GMT+0100 (Центральная Европа, стандартное время)',
112      'Fri Jul 25 1980 01:35:33 GMT+0100 (Horário Padrão da Europa Central)',
113      'Fri Jul 25 1980 01:35:33 GMT+0100 (وسطی یورپ کا معیاری وقت)',
114      'Fri Jul 25 1980 01:35:33 GMT+0100 (Waktu Standar Eropa Tengah)',
115      'Fri Jul 25 1980 01:35:33 GMT+0100 (Mitteleuropäische Normalzeit)',
116      'Fri Jul 25 1980 01:35:33 GMT+0100 (中央ヨーロッパ標準時)',
117      'Fri Jul 25 1980 01:35:33 GMT+0100 (Mídúl Yúrop Fíksd Taim)',
118      'Fri Jul 25 1980 01:35:33 GMT+0100 (मध्‍य युरोपियन प्रमाण वेळ)',
119      'Fri Jul 25 1980 01:35:33 GMT+0100 (సెంట్రల్ యూరోపియన్ ప్రామాణిక సమయం)',
120    ]
121  );
122  assert.deepStrictEqual(
123    locales.map((LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Date(333333333333).toLocaleString()')),
124    [
125      '7/25/1980, 1:35:33 AM',
126      '1980/7/25 01:35:33',
127      '25/7/1980, 1:35:33 am',
128      '25/7/1980, 1:35:33',
129      '25/07/1980 01:35:33',
130      '٢٥‏/٧‏/١٩٨٠، ١:٣٥:٣٣ ص',
131      '২৫/৭/১৯৮০, ১:৩৫:৩৩ AM',
132      '25.07.1980, 01:35:33',
133      '25/07/1980, 01:35:33',
134      '25/7/1980، 1:35:33 AM',
135      '25/7/1980, 01.35.33',
136      '25.7.1980, 01:35:33',
137      '1980/7/25 1:35:33',
138      '25/7/1980 01:35:33',
139      '२५/७/१९८०, १:३५:३३ AM',
140      '25/7/1980 1:35:33 AM',
141    ]
142  );
143  assert.strictEqual(
144    runEnvOutside({ LANG: 'en' }, '["z", "ä"].sort(new Intl.Collator().compare)'),
145    'ä,z'
146  );
147  assert.strictEqual(
148    runEnvOutside({ LANG: 'sv' }, '["z", "ä"].sort(new Intl.Collator().compare)'),
149    'z,ä'
150  );
151  assert.deepStrictEqual(
152    locales.map(
153      (LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Intl.DateTimeFormat().format(333333333333)')
154    ),
155    [
156      '7/25/1980', '1980/7/25',
157      '25/7/1980', '25/7/1980',
158      '25/07/1980', '٢٥‏/٧‏/١٩٨٠',
159      '২৫/৭/১৯৮০', '25.07.1980',
160      '25/07/1980', '25/7/1980',
161      '25/7/1980', '25.7.1980',
162      '1980/7/25', '25/7/1980',
163      '२५/७/१९८०', '25/7/1980',
164    ]
165  );
166  assert.deepStrictEqual(
167    locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.DisplayNames(undefined, { type: "region" }).of("CH")')),
168    [
169      'Switzerland', '瑞士',
170      'स्विट्ज़रलैंड', 'Suiza',
171      'Suisse', 'سويسرا',
172      'সুইজারল্যান্ড', 'Швейцария',
173      'Suíça', 'سوئٹزر لینڈ',
174      'Swiss', 'Schweiz',
175      'スイス', 'Swítsaland',
176      'स्वित्झर्लंड', 'స్విట్జర్లాండ్',
177    ]
178  );
179  assert.deepStrictEqual(
180    locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.NumberFormat().format(275760.913)')),
181    [
182      '275,760.913', '275,760.913',
183      '2,75,760.913', '275.760,913',
184      '275 760,913', '٢٧٥٬٧٦٠٫٩١٣',
185      '২,৭৫,৭৬০.৯১৩', '275 760,913',
186      '275.760,913', '275,760.913',
187      '275.760,913', '275.760,913',
188      '275,760.913', '275,760.913',
189      '२,७५,७६०.९१३', '2,75,760.913',
190    ]
191  );
192  assert.deepStrictEqual(
193    locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.PluralRules().select(0)')),
194    [
195      'other', 'other', 'one', 'other',
196      'one', 'zero', 'one', 'many',
197      'one', 'other', 'other', 'other',
198      'other', 'one', 'other', 'other',
199    ]
200  );
201  assert.deepStrictEqual(
202    locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.RelativeTimeFormat().format(-586920.617, "hour")')),
203    [
204      '586,920.617 hours ago',
205      '586,920.617小时前',
206      '5,86,920.617 घंटे पहले',
207      'hace 586.920,617 horas',
208      'il y a 586 920,617 heures',
209      'قبل ٥٨٦٬٩٢٠٫٦١٧ ساعة',
210      '৫,৮৬,৯২০.৬১৭ ঘন্টা আগে',
211      '586 920,617 часа назад',
212      'há 586.920,617 horas',
213      '586,920.617 گھنٹے پہلے',
214      '586.920,617 jam yang lalu',
215      'vor 586.920,617 Stunden',
216      '586,920.617 時間前',
217      '586,920.617 áwa wé dọ́n pas',
218      '५,८६,९२०.६१७ तासांपूर्वी',
219      '5,86,920.617 గంటల క్రితం',
220    ]
221  );
222}
223
224
225// Tests with process.env mutated inside
226{
227  // process.env.TZ is not intercepted in Workers
228  if (common.isMainThread) {
229    assert.strictEqual(
230      isSet(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toString()))),
231      true
232    );
233    assert.strictEqual(
234      isSet(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toLocaleString()))),
235      true
236    );
237  } else {
238    assert.strictEqual(
239      isPack(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toString()))),
240      true
241    );
242    assert.strictEqual(
243      isPack(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toLocaleString()))),
244      true
245    );
246  }
247
248  assert.strictEqual(
249    isPack(locales.map((LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Date(333333333333).toString()))),
250    true
251  );
252  assert.strictEqual(
253    isPack(locales.map(
254      (LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Date(333333333333).toLocaleString())
255    )),
256    true
257  );
258  assert.deepStrictEqual(
259    runEnvInside({ LANG: 'en' }, () => ['z', 'ä'].sort(new Intl.Collator().compare)),
260    runEnvInside({ LANG: 'sv' }, () => ['z', 'ä'].sort(new Intl.Collator().compare))
261  );
262  assert.strictEqual(
263    isPack(locales.map(
264      (LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Intl.DateTimeFormat().format(333333333333))
265    )),
266    true
267  );
268  assert.strictEqual(
269    isPack(locales.map(
270      (LANG) => runEnvInside({ LANG }, () => new Intl.DisplayNames(undefined, { type: 'region' }).of('CH'))
271    )),
272    true
273  );
274  assert.strictEqual(
275    isPack(locales.map((LANG) => runEnvInside({ LANG }, () => new Intl.NumberFormat().format(275760.913)))),
276    true
277  );
278  assert.strictEqual(
279    isPack(locales.map((LANG) => runEnvInside({ LANG }, () => new Intl.PluralRules().select(0)))),
280    true
281  );
282  assert.strictEqual(
283    isPack(locales.map(
284      (LANG) => runEnvInside({ LANG }, () => new Intl.RelativeTimeFormat().format(-586920.617, 'hour'))
285    )),
286    true
287  );
288}
289