@munchi_oy/core 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/api.d.ts +373 -115
- package/dist/generated/api.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/src/currency.d.ts +2 -1
- package/dist/src/currency.d.ts.map +1 -1
- package/dist/src/money.d.ts +28 -0
- package/dist/src/money.d.ts.map +1 -0
- package/dist/src/region.d.ts +11 -0
- package/dist/src/region.d.ts.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/generated/api.ts +377 -119
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/currency.ts +4 -15
- package/src/money.test.ts +158 -0
- package/src/money.ts +125 -0
- package/src/region.test.ts +80 -0
- package/src/region.ts +61 -0
- package/src/version.ts +1 -1
package/index.ts
CHANGED
package/package.json
CHANGED
package/src/currency.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { CurrencyCode } from "../generated";
|
|
2
|
-
|
|
3
|
-
const currencyMinorUnits: Record<string, number> = {
|
|
4
|
-
[CurrencyCode.Eur]: 2,
|
|
5
|
-
[CurrencyCode.Aed]: 2,
|
|
6
|
-
[CurrencyCode.Usd]: 2,
|
|
7
|
-
[CurrencyCode.Gbp]: 2,
|
|
8
|
-
BHD: 3,
|
|
9
|
-
JPY: 0,
|
|
10
|
-
};
|
|
1
|
+
import type { CurrencyCode } from "../generated";
|
|
2
|
+
import { minorUnitMultiplier } from "./money";
|
|
11
3
|
|
|
4
|
+
/** @deprecated Use `minorUnitMultiplier`, `toMinor` and `fromMinor` from `./money`. */
|
|
12
5
|
export const CurrencyRegistry = {
|
|
13
6
|
getMultiplier(currencyCode: CurrencyCode | string): number {
|
|
14
|
-
|
|
15
|
-
if (typeof minorUnits !== "number") {
|
|
16
|
-
return 100;
|
|
17
|
-
}
|
|
18
|
-
return Math.round(10 ** minorUnits);
|
|
7
|
+
return minorUnitMultiplier(currencyCode);
|
|
19
8
|
},
|
|
20
9
|
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { CurrencyCode } from "../generated";
|
|
2
|
+
import {
|
|
3
|
+
cashRound,
|
|
4
|
+
formatMoney,
|
|
5
|
+
fromMinor,
|
|
6
|
+
minorUnitDigits,
|
|
7
|
+
toMinor,
|
|
8
|
+
} from "./money";
|
|
9
|
+
|
|
10
|
+
describe("minorUnitDigits", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
[CurrencyCode.Eur, 2],
|
|
13
|
+
[CurrencyCode.Sek, 2],
|
|
14
|
+
[CurrencyCode.Aed, 2],
|
|
15
|
+
[CurrencyCode.Isk, 0],
|
|
16
|
+
["JPY", 0],
|
|
17
|
+
["BHD", 3],
|
|
18
|
+
["kwd", 3],
|
|
19
|
+
["CLF", 4],
|
|
20
|
+
["UYW", 4],
|
|
21
|
+
])("gives %s %i decimals", (currency, digits) => {
|
|
22
|
+
expect(minorUnitDigits(currency)).toBe(digits);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("toMinor and fromMinor", () => {
|
|
27
|
+
it("converts a two-decimal price without float drift", () => {
|
|
28
|
+
expect(toMinor(19.99, CurrencyCode.Eur)).toBe(1999);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("converts a zero-decimal currency one to one", () => {
|
|
32
|
+
expect(toMinor(1500, CurrencyCode.Isk)).toBe(1500);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("converts a three-decimal currency in thousandths", () => {
|
|
36
|
+
expect(toMinor(1.234, "BHD")).toBe(1234);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
[1.005, CurrencyCode.Eur, 101],
|
|
41
|
+
[1.015, CurrencyCode.Eur, 102],
|
|
42
|
+
[2.5, "JPY", 3],
|
|
43
|
+
[1.0005, "KWD", 1001],
|
|
44
|
+
[1.00005, "CLF", 10001],
|
|
45
|
+
])(
|
|
46
|
+
"rounds the half-way amount %d %s up to %i minor units",
|
|
47
|
+
(major, currency, minor) => {
|
|
48
|
+
expect(toMinor(major, currency)).toBe(minor);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it.each([
|
|
53
|
+
[-1.005, CurrencyCode.Eur, -100],
|
|
54
|
+
[-1.015, CurrencyCode.Eur, -101],
|
|
55
|
+
[-2.5, "JPY", -2],
|
|
56
|
+
[-1.0005, "KWD", -1000],
|
|
57
|
+
[-1.00005, "CLF", -10000],
|
|
58
|
+
])(
|
|
59
|
+
"rounds the negative half-way amount %d %s toward +Infinity like Math.round, to %i",
|
|
60
|
+
(major, currency, minor) => {
|
|
61
|
+
expect(toMinor(major, currency)).toBe(minor);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it.each([
|
|
66
|
+
[1.006, CurrencyCode.Eur, 101],
|
|
67
|
+
[-1.006, CurrencyCode.Eur, -101],
|
|
68
|
+
])(
|
|
69
|
+
"rounds %d %s past the half way to the nearer unit, %i",
|
|
70
|
+
(major, currency, minor) => {
|
|
71
|
+
expect(toMinor(major, currency)).toBe(minor);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it("converts zero to zero", () => {
|
|
76
|
+
expect(toMinor(0, CurrencyCode.Eur)).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("converts amounts written in exponent notation", () => {
|
|
80
|
+
expect(toMinor(1e21, CurrencyCode.Eur)).toBe(1e23);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("converts a tiny amount in exponent notation to zero minor units", () => {
|
|
84
|
+
expect(toMinor(1e-7, CurrencyCode.Eur)).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps a non-finite amount non-finite", () => {
|
|
88
|
+
expect(toMinor(Number.POSITIVE_INFINITY, CurrencyCode.Eur)).toBe(
|
|
89
|
+
Number.POSITIVE_INFINITY,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("converts a four-decimal currency in ten-thousandths", () => {
|
|
94
|
+
expect(toMinor(1.2345, "CLF")).toBe(12345);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("turns minor units back into the major amount", () => {
|
|
98
|
+
expect(fromMinor(1999, CurrencyCode.Eur)).toBe(19.99);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("formatMoney", () => {
|
|
103
|
+
it("puts the euro sign after the amount in Finnish", () => {
|
|
104
|
+
expect(
|
|
105
|
+
formatMoney({ amount: 1250, currency: CurrencyCode.Eur }, "fi-FI"),
|
|
106
|
+
).toMatch(/^12,50\s€$/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("puts the pound sign before the amount in British English", () => {
|
|
110
|
+
expect(
|
|
111
|
+
formatMoney({ amount: 1250, currency: CurrencyCode.Gbp }, "en-GB"),
|
|
112
|
+
).toBe("£12.50");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("shows no decimals for a zero-decimal currency", () => {
|
|
116
|
+
expect(
|
|
117
|
+
formatMoney({ amount: 1500, currency: CurrencyCode.Isk }, "en-GB", {
|
|
118
|
+
currencyDisplay: "code",
|
|
119
|
+
}),
|
|
120
|
+
).toMatch(/^ISK\s1,500$/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("shows three decimals for a three-decimal currency", () => {
|
|
124
|
+
expect(
|
|
125
|
+
formatMoney({ amount: 1234, currency: "BHD" }, "en-GB", {
|
|
126
|
+
currencyDisplay: "code",
|
|
127
|
+
}),
|
|
128
|
+
).toMatch(/^BHD\s1\.234$/);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("cashRound", () => {
|
|
133
|
+
it.each([
|
|
134
|
+
[1002, 1000],
|
|
135
|
+
[1003, 1005],
|
|
136
|
+
[1007, 1005],
|
|
137
|
+
[1008, 1010],
|
|
138
|
+
[1005, 1005],
|
|
139
|
+
])("rounds %i to %i with a five-cent increment", (amount, rounded) => {
|
|
140
|
+
expect(cashRound(amount, { increment: 5 }).roundedAmount).toBe(rounded);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("reports the rounding difference", () => {
|
|
144
|
+
expect(cashRound(1003, { increment: 5 }).roundingDifference).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("leaves the amount alone without cash rounding", () => {
|
|
148
|
+
expect(cashRound(1003, null)).toEqual({
|
|
149
|
+
actualAmount: 1003,
|
|
150
|
+
roundedAmount: 1003,
|
|
151
|
+
roundingDifference: 0,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("leaves refunds and zero alone", () => {
|
|
156
|
+
expect(cashRound(-1003, { increment: 5 }).roundedAmount).toBe(-1003);
|
|
157
|
+
});
|
|
158
|
+
});
|
package/src/money.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { CurrencyCode } from "../generated";
|
|
2
|
+
|
|
3
|
+
/** An amount in the currency's minor unit (cents for EUR), always an integer. */
|
|
4
|
+
export interface Money {
|
|
5
|
+
amount: number;
|
|
6
|
+
currency: CurrencyCode | string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CashRounding {
|
|
10
|
+
/** Cash totals snap to a multiple of this many minor units (5 = 0.05 EUR). */
|
|
11
|
+
increment: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CashRoundingResult {
|
|
15
|
+
actualAmount: number;
|
|
16
|
+
roundedAmount: number;
|
|
17
|
+
roundingDifference: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ISO 4217 minor units that differ from 2. A table, not Intl, so every runtime (Hermes, Node, browsers) agrees.
|
|
21
|
+
const MINOR_UNIT_DIGITS: Record<string, number> = {
|
|
22
|
+
BIF: 0,
|
|
23
|
+
CLP: 0,
|
|
24
|
+
DJF: 0,
|
|
25
|
+
GNF: 0,
|
|
26
|
+
ISK: 0,
|
|
27
|
+
JPY: 0,
|
|
28
|
+
KMF: 0,
|
|
29
|
+
KRW: 0,
|
|
30
|
+
PYG: 0,
|
|
31
|
+
RWF: 0,
|
|
32
|
+
UGX: 0,
|
|
33
|
+
UYI: 0,
|
|
34
|
+
VND: 0,
|
|
35
|
+
VUV: 0,
|
|
36
|
+
XAF: 0,
|
|
37
|
+
XOF: 0,
|
|
38
|
+
XPF: 0,
|
|
39
|
+
BHD: 3,
|
|
40
|
+
IQD: 3,
|
|
41
|
+
JOD: 3,
|
|
42
|
+
KWD: 3,
|
|
43
|
+
LYD: 3,
|
|
44
|
+
OMR: 3,
|
|
45
|
+
TND: 3,
|
|
46
|
+
CLF: 4,
|
|
47
|
+
UYW: 4,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function minorUnitDigits(currency: CurrencyCode | string): number {
|
|
51
|
+
return MINOR_UNIT_DIGITS[currency.toUpperCase()] ?? 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function minorUnitMultiplier(currency: CurrencyCode | string): number {
|
|
55
|
+
return 10 ** minorUnitDigits(currency);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Shifts the decimal point in the number's own decimal text, so 1.005 EUR is 100.5 cents and rounds to 101;
|
|
59
|
+
// `1.005 * 100` is 100.49999999999999 in binary floating point. Ties round like Math.round (toward +Infinity).
|
|
60
|
+
export function toMinor(
|
|
61
|
+
major: number,
|
|
62
|
+
currency: CurrencyCode | string,
|
|
63
|
+
): number {
|
|
64
|
+
if (!Number.isFinite(major)) {
|
|
65
|
+
return Math.round(major * minorUnitMultiplier(currency));
|
|
66
|
+
}
|
|
67
|
+
const [mantissa, exponent = "0"] = String(major).split("e");
|
|
68
|
+
return Math.round(
|
|
69
|
+
Number(`${mantissa}e${Number(exponent) + minorUnitDigits(currency)}`),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function fromMinor(
|
|
74
|
+
minor: number,
|
|
75
|
+
currency: CurrencyCode | string,
|
|
76
|
+
): number {
|
|
77
|
+
return minor / minorUnitMultiplier(currency);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FormatMoneyOptions {
|
|
81
|
+
currencyDisplay?: "symbol" | "narrowSymbol" | "code" | "name";
|
|
82
|
+
signDisplay?: "auto" | "always" | "exceptZero" | "never";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Formats with Intl in `locale` (e.g. "fi-FI", "en-FI"), so the symbol sits where that locale puts it. */
|
|
86
|
+
export function formatMoney(
|
|
87
|
+
money: Money,
|
|
88
|
+
locale: string,
|
|
89
|
+
options: FormatMoneyOptions = {},
|
|
90
|
+
): string {
|
|
91
|
+
const digits = minorUnitDigits(money.currency);
|
|
92
|
+
return new Intl.NumberFormat(locale, {
|
|
93
|
+
style: "currency",
|
|
94
|
+
currency: money.currency.toUpperCase(),
|
|
95
|
+
currencyDisplay: options.currencyDisplay ?? "symbol",
|
|
96
|
+
signDisplay: options.signDisplay ?? "auto",
|
|
97
|
+
minimumFractionDigits: digits,
|
|
98
|
+
maximumFractionDigits: digits,
|
|
99
|
+
}).format(fromMinor(money.amount, money.currency));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Rounds a cash total half up to the increment; `null` means no cash rounding. */
|
|
103
|
+
export function cashRound(
|
|
104
|
+
amount: number,
|
|
105
|
+
rounding: CashRounding | null,
|
|
106
|
+
): CashRoundingResult {
|
|
107
|
+
const increment = rounding?.increment ?? 0;
|
|
108
|
+
if (amount <= 0 || !Number.isFinite(amount) || increment <= 1) {
|
|
109
|
+
return {
|
|
110
|
+
actualAmount: amount,
|
|
111
|
+
roundedAmount: amount,
|
|
112
|
+
roundingDifference: 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const remainder = amount % increment;
|
|
116
|
+
const roundedAmount =
|
|
117
|
+
remainder * 2 < increment
|
|
118
|
+
? amount - remainder
|
|
119
|
+
: amount + (increment - remainder);
|
|
120
|
+
return {
|
|
121
|
+
actualAmount: amount,
|
|
122
|
+
roundedAmount,
|
|
123
|
+
roundingDifference: roundedAmount - amount,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cashRound } from "./money";
|
|
2
|
+
import { countryQuirks, regionLocale } from "./region";
|
|
3
|
+
|
|
4
|
+
describe("countryQuirks", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
["FI", 5],
|
|
7
|
+
["EE", 5],
|
|
8
|
+
["SE", 100],
|
|
9
|
+
])("rounds cash in %s to %i minor units", (countryCode, increment) => {
|
|
10
|
+
expect(countryQuirks(countryCode).cashRounding).toEqual({ increment });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each(["DE", "US", "JP"])("does not round cash in %s", (countryCode) => {
|
|
14
|
+
expect(countryQuirks(countryCode).cashRounding).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("feeds cash rounding straight into cashRound", () => {
|
|
18
|
+
expect(
|
|
19
|
+
cashRound(1003, countryQuirks("FI").cashRounding).roundedAmount,
|
|
20
|
+
).toBe(1005);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
["DE", ["de", "en"]],
|
|
25
|
+
["FR", ["fr", "en"]],
|
|
26
|
+
["JP", ["ja", "en"]],
|
|
27
|
+
])(
|
|
28
|
+
"offers customers in %s the country's language, then English",
|
|
29
|
+
(countryCode, languages) => {
|
|
30
|
+
expect(countryQuirks(countryCode).customerLanguages).toEqual(languages);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
it("offers English once where English is the country's language", () => {
|
|
35
|
+
expect(countryQuirks("US").customerLanguages).toEqual(["en"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it.each([
|
|
39
|
+
["FI", ["fi", "sv", "en"]],
|
|
40
|
+
["EE", ["et", "ru", "en"]],
|
|
41
|
+
["ES", ["es", "ca", "en"]],
|
|
42
|
+
])(
|
|
43
|
+
"offers customers in %s the languages listed for it",
|
|
44
|
+
(countryCode, languages) => {
|
|
45
|
+
expect(countryQuirks(countryCode).customerLanguages).toEqual(languages);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it("reads the country code in any case", () => {
|
|
50
|
+
expect(countryQuirks("fi")).toEqual(countryQuirks("FI"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([null, undefined, ""])(
|
|
54
|
+
"falls back to English and no rounding without a country (%p)",
|
|
55
|
+
(countryCode) => {
|
|
56
|
+
expect(countryQuirks(countryCode)).toEqual({
|
|
57
|
+
cashRounding: null,
|
|
58
|
+
customerLanguages: ["en"],
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
it("falls back to English for a code that is not a country", () => {
|
|
64
|
+
expect(countryQuirks("not-a-country").customerLanguages).toEqual(["en"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("regionLocale", () => {
|
|
69
|
+
it("combines the reader's language with the venue's country", () => {
|
|
70
|
+
expect(regionLocale("en", "FI")).toBe("en-FI");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("drops a region the language already carries", () => {
|
|
74
|
+
expect(regionLocale("fi-FI", "FI")).toBe("fi-FI");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to the bare language without a country", () => {
|
|
78
|
+
expect(regionLocale("sv", null)).toBe("sv");
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/region.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CashRounding } from "./money";
|
|
2
|
+
|
|
3
|
+
/** What can't be derived from a store's country with Intl. Every country is allowed; this only fills in local habits. */
|
|
4
|
+
export interface CountryQuirks {
|
|
5
|
+
cashRounding: CashRounding | null;
|
|
6
|
+
/** Languages a customer-facing screen (kiosk, receipt) offers; the first is the default. */
|
|
7
|
+
customerLanguages: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const QUIRKS: Readonly<Record<string, Partial<CountryQuirks>>> = {
|
|
11
|
+
FI: { cashRounding: { increment: 5 }, customerLanguages: ["fi", "sv", "en"] },
|
|
12
|
+
EE: { cashRounding: { increment: 5 }, customerLanguages: ["et", "ru", "en"] },
|
|
13
|
+
SE: { cashRounding: { increment: 100 } },
|
|
14
|
+
ES: { customerLanguages: ["es", "ca", "en"] },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Intl.Locale is missing on some runtimes (older Hermes), and an unknown region throws.
|
|
18
|
+
function mainLanguage(countryCode: string): string | undefined {
|
|
19
|
+
if (typeof Intl.Locale !== "function") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return new Intl.Locale(`und-${countryCode}`).maximize().language;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function countryQuirks(
|
|
30
|
+
countryCode: string | null | undefined,
|
|
31
|
+
): CountryQuirks {
|
|
32
|
+
if (!countryCode) {
|
|
33
|
+
return { cashRounding: null, customerLanguages: ["en"] };
|
|
34
|
+
}
|
|
35
|
+
const code = countryCode.toUpperCase();
|
|
36
|
+
const quirks = QUIRKS[code] ?? {};
|
|
37
|
+
const language = mainLanguage(code);
|
|
38
|
+
return {
|
|
39
|
+
cashRounding: quirks.cashRounding ?? null,
|
|
40
|
+
customerLanguages:
|
|
41
|
+
quirks.customerLanguages ??
|
|
42
|
+
(language && language !== "und"
|
|
43
|
+
? [...new Set([language, "en"])]
|
|
44
|
+
: ["en"]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The locale to format numbers and dates in: the reader's language with the venue's country ("en" + "FI" → "en-FI"). */
|
|
49
|
+
export function regionLocale(
|
|
50
|
+
language: string,
|
|
51
|
+
countryCode: string | null | undefined,
|
|
52
|
+
): string {
|
|
53
|
+
const base = language.split("-")[0].toLowerCase();
|
|
54
|
+
if (!countryCode) {
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
const candidate = `${base}-${countryCode.toUpperCase()}`;
|
|
58
|
+
return Intl.NumberFormat.supportedLocalesOf([candidate]).length > 0
|
|
59
|
+
? candidate
|
|
60
|
+
: base;
|
|
61
|
+
}
|
package/src/version.ts
CHANGED