Skip to content

Commit fe4f31b

Browse files
committed
lib: set ICU locale per cli using --icu-locale
1 parent d0f5943 commit fe4f31b

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

doc/api/cli.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,19 @@ added: v0.1.3
13641364
Print node command-line options.
13651365
The output of this option is less detailed than this document.
13661366

1367+
### `--icu-locale`
1368+
1369+
<!-- YAML
1370+
added: REPLACEME
1371+
-->
1372+
1373+
Set the default locale used by ICU (`Intl` object). It expects a string
1374+
representing the language version as defined in [RFC 5646][] (also known as
1375+
BCP 47).
1376+
1377+
Examples of valid language codes include "en", "en-US", "fr", "fr-FR", "es-ES",
1378+
etc.
1379+
13671380
### `--icu-data-dir=file`
13681381

13691382
<!-- YAML
@@ -2948,6 +2961,7 @@ one is included in the list below.
29482961
* `--heapsnapshot-signal`
29492962
* `--http-parser`
29502963
* `--icu-data-dir`
2964+
* `--icu-locale`
29512965
* `--import`
29522966
* `--input-type`
29532967
* `--insecure-http-parser`
@@ -3457,6 +3471,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
34573471
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
34583472
[Permission Model]: permissions.md#permission-model
34593473
[REPL]: repl.md
3474+
[RFC 5646]: https://www.rfc-editor.org/rfc/rfc5646.txt
34603475
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
34613476
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm
34623477
[Source Map]: https://sourcemaps.info/spec.html

src/node.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,10 @@ static ExitCode InitializeNodeWithArgsInternal(
963963
}
964964
# endif
965965

966+
if (!per_process::cli_options->icu_locale.empty()) {
967+
i18n::SetDefaultLocale(per_process::cli_options->icu_locale.c_str());
968+
}
969+
966970
#endif // defined(NODE_HAVE_I18N_SUPPORT)
967971

968972
// We should set node_is_initialized here instead of in node::Start,

src/node_i18n.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@
5454
#include "util-inl.h"
5555
#include "v8.h"
5656

57+
#include <unicode/locid.h>
5758
#include <unicode/putil.h>
5859
#include <unicode/timezone.h>
5960
#include <unicode/uchar.h>
6061
#include <unicode/uclean.h>
6162
#include <unicode/ucnv.h>
6263
#include <unicode/udata.h>
6364
#include <unicode/uidna.h>
65+
#include <unicode/uloc.h>
6466
#include <unicode/ulocdata.h>
6567
#include <unicode/urename.h>
6668
#include <unicode/ustring.h>
@@ -602,6 +604,37 @@ void SetDefaultTimeZone(const char* tzid) {
602604
CHECK(U_SUCCESS(status));
603605
}
604606

607+
void SetDefaultLocale(const char* localeid) {
608+
UErrorCode status = U_ZERO_ERROR;
609+
610+
// Set the locale to the requested locale, no matter if it is
611+
// supported or not. Let ICU handle the conversion to a supported locale.
612+
// E.g. "en_US" and "en-US" will be internally converted to "en_US".
613+
uloc_setDefault(localeid, &status);
614+
CHECK(U_SUCCESS(status));
615+
616+
// Now check if the locale was actually set to a supported locale.
617+
// We iterate over all supported locales and check if the default locale
618+
// is among them. If so, we can finish here.
619+
const char* newDefaultLocale = uloc_getDefault();
620+
int newDefaultLocaleLen = strlen(newDefaultLocale);
621+
int32_t locCount = 0;
622+
const icu::Locale* supportedLocales =
623+
icu::Locale::getAvailableLocales(locCount);
624+
for (int32_t i = 0; i < locCount; ++i) {
625+
if (strncmp(newDefaultLocale,
626+
supportedLocales[i].getName(),
627+
newDefaultLocaleLen) == 0) {
628+
return;
629+
}
630+
}
631+
632+
// The default locale is not supported. We need to set it to a supported
633+
// locale. We use the root locale as a fallback.
634+
uloc_setDefault(nullptr, &status);
635+
CHECK(U_SUCCESS(status));
636+
}
637+
605638
int32_t ToUnicode(MaybeStackBuffer<char>* buf,
606639
const char* input,
607640
size_t length) {

src/node_i18n.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ bool InitializeICUDirectory(const std::string& path, std::string* error);
4242

4343
void SetDefaultTimeZone(const char* tzid);
4444

45+
void SetDefaultLocale(const char* localid);
46+
4547
enum class idna_mode {
4648
// Default mode for maximum compatibility.
4749
kDefault,

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
10181018
,
10191019
&PerProcessOptions::icu_data_dir,
10201020
kAllowedInEnvvar);
1021+
1022+
AddOption("--icu-locale",
1023+
"Set the locale of the ICU used by the node instance",
1024+
&PerProcessOptions::icu_locale,
1025+
kAllowedInEnvvar);
10211026
#endif
10221027

10231028
#if HAVE_OPENSSL

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ class PerProcessOptions : public Options {
313313

314314
#ifdef NODE_HAVE_I18N_SUPPORT
315315
std::string icu_data_dir;
316+
std::string icu_locale;
316317
#endif
317318

318319
// Per-process because they affect singleton OpenSSL shared library state,

test/parallel/test-icu-locale.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
4+
const assert = require('assert');
5+
6+
if (!common.hasIntl)
7+
common.skip('Intl not present.');
8+
{
9+
const { child } = spawnSyncAndExitWithoutError(
10+
process.execPath,
11+
[
12+
'--icu-locale=fr-FR',
13+
'-p',
14+
'Intl?.Collator().resolvedOptions().locale',
15+
]);
16+
assert.strictEqual(child.stdout.toString(), 'fr-FR\n');
17+
}
18+
19+
{
20+
const { child } = spawnSyncAndExitWithoutError(
21+
process.execPath,
22+
[
23+
'--icu-locale=en-GB',
24+
'-p',
25+
'Intl?.Collator().resolvedOptions().locale',
26+
]);
27+
assert.strictEqual(child.stdout.toString(), 'en-GB\n');
28+
}
29+
30+
{
31+
const { child } = spawnSyncAndExitWithoutError(
32+
process.execPath,
33+
[
34+
'--icu-locale=en_GB',
35+
'-p',
36+
'Intl?.Collator().resolvedOptions().locale',
37+
]);
38+
assert.strictEqual(child.stdout.toString(), 'en-GB\n');
39+
}
40+
41+
{
42+
const { child } = spawnSyncAndExitWithoutError(
43+
process.execPath,
44+
[
45+
'--icu-locale=en',
46+
'-p',
47+
'Intl?.Collator().resolvedOptions().locale',
48+
]);
49+
assert.strictEqual(child.stdout.toString(), 'en\n');
50+
}
51+
52+
{
53+
const { child } = spawnSyncAndExitWithoutError(
54+
process.execPath,
55+
[
56+
'--icu-locale=de-DE',
57+
'-p',
58+
'Intl?.Collator().resolvedOptions().locale',
59+
]);
60+
assert.strictEqual(child.stdout.toString(), 'de-DE\n');
61+
}
62+
63+
{
64+
const { child } = spawnSyncAndExitWithoutError(
65+
process.execPath,
66+
[
67+
'--icu-locale=invalid',
68+
'-p',
69+
'Intl?.Collator().resolvedOptions().locale',
70+
]);
71+
assert.strictEqual(child.stdout.toString(), 'en-US\n');
72+
}
73+
74+
// NumberFormat
75+
{
76+
const { child } = spawnSyncAndExitWithoutError(
77+
process.execPath,
78+
['--icu-locale=de-DE', '-p', '(123456.789).toLocaleString(undefined, { style: "currency", currency: "EUR" })']
79+
);
80+
assert.strictEqual(child.stdout.toString(), '123.456,79\xa0€\n');
81+
}
82+
83+
{
84+
const { child } = spawnSyncAndExitWithoutError(
85+
process.execPath,
86+
[
87+
'--icu-locale=ja-JP',
88+
'-p',
89+
'(123456.789).toLocaleString(undefined, { style: "currency", currency: "JPY" })',
90+
]);
91+
assert.strictEqual(child.stdout.toString(), '¥123,457\n');
92+
}
93+
94+
{
95+
const { child } = spawnSyncAndExitWithoutError(
96+
process.execPath,
97+
[
98+
'--icu-locale=en-IN',
99+
'-p',
100+
'(123456.789).toLocaleString(undefined, { maximumSignificantDigits: 3 })',
101+
]);
102+
assert.strictEqual(child.stdout.toString(), '1,23,000\n');
103+
}
104+
// DateTimeFormat
105+
{
106+
const { child } = spawnSyncAndExitWithoutError(
107+
process.execPath,
108+
[
109+
'--icu-locale=en-GB',
110+
'-p',
111+
'new Date(Date.UTC(2012, 11, 20, 3, 0, 0)).toLocaleString(undefined, { timeZone: "UTC" })',
112+
]);
113+
assert.strictEqual(child.stdout.toString(), '20/12/2012, 03:00:00\n');
114+
}
115+
116+
{
117+
const { child } = spawnSyncAndExitWithoutError(
118+
process.execPath,
119+
[
120+
'--icu-locale=ko-KR',
121+
'-p',
122+
'new Date(Date.UTC(2012, 11, 20, 3, 0, 0)).toLocaleString(undefined, { timeZone: "UTC" })',
123+
]);
124+
assert.strictEqual(child.stdout.toString(), '2012. 12. 20. 오전 3:00:00\n');
125+
}
126+
127+
// ListFormat
128+
{
129+
const { child } = spawnSyncAndExitWithoutError(
130+
process.execPath,
131+
[
132+
'--icu-locale=en-US',
133+
'-p',
134+
'new Intl.ListFormat(undefined, {style: "long", type:"conjunction"}).format(["a", "b", "c"])',
135+
]);
136+
assert.strictEqual(child.stdout.toString(), 'a, b, and c\n');
137+
}
138+
139+
{
140+
const { child } = spawnSyncAndExitWithoutError(
141+
process.execPath,
142+
[
143+
'--icu-locale=de-DE',
144+
'-p',
145+
'new Intl.ListFormat(undefined, {style: "long", type:"conjunction"}).format(["a", "b", "c"])',
146+
]);
147+
assert.strictEqual(child.stdout.toString(), 'a, b und c\n');
148+
}
149+
150+
// RelativeTimeFormat
151+
{
152+
const { child } = spawnSyncAndExitWithoutError(
153+
process.execPath,
154+
[
155+
'--icu-locale=en',
156+
'-p',
157+
'new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(3, "quarter")',
158+
]);
159+
assert.strictEqual(child.stdout.toString(), 'in 3 quarters\n');
160+
}
161+
{
162+
const { child } = spawnSyncAndExitWithoutError(
163+
process.execPath,
164+
[
165+
'--icu-locale=es',
166+
'-p',
167+
'new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(2, "day")',
168+
]);
169+
assert.strictEqual(child.stdout.toString(), 'pasado mañana\n');
170+
}
171+
172+
// Collator
173+
{
174+
const { child } = spawnSyncAndExitWithoutError(
175+
process.execPath,
176+
[
177+
'--icu-locale=de',
178+
'-p',
179+
'["Z", "a", "z", "ä"].sort(new Intl.Collator().compare).join(",")',
180+
]);
181+
assert.strictEqual(child.stdout.toString(), 'a,ä,z,Z\n');
182+
}
183+
{
184+
const { child } = spawnSyncAndExitWithoutError(
185+
process.execPath,
186+
[
187+
'--icu-locale=sv',
188+
'-p',
189+
'["Z", "a", "z", "ä"].sort(new Intl.Collator().compare).join(",")',
190+
]);
191+
assert.strictEqual(child.stdout.toString(), 'a,z,Z,ä\n');
192+
}
193+
194+
{
195+
const { child } = spawnSyncAndExitWithoutError(
196+
process.execPath,
197+
[
198+
'--icu-locale=de',
199+
'-p',
200+
'["Z", "a", "z", "ä"].sort(new Intl.Collator(undefined, { caseFirst: "upper" }).compare).join(",")',
201+
]);
202+
assert.strictEqual(child.stdout.toString(), 'a,ä,Z,z\n');
203+
}

0 commit comments

Comments
 (0)