@doswiftly/storefront-sdk 15.1.0 → 16.0.0
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/CHANGELOG.md +239 -0
- package/README.md +28 -0
- package/dist/core/cart/types.d.ts +1 -1
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/format.d.ts +81 -46
- package/dist/core/format.d.ts.map +1 -1
- package/dist/core/format.js +116 -94
- package/dist/core/generated/operation-types.d.ts +75 -17
- package/dist/core/generated/operation-types.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +11 -0
- package/dist/react/components/Money.d.ts +22 -8
- package/dist/react/components/Money.d.ts.map +1 -1
- package/dist/react/components/Money.js +16 -9
- package/dist/react/hooks/use-format.d.ts +85 -0
- package/dist/react/hooks/use-format.d.ts.map +1 -0
- package/dist/react/hooks/use-format.js +141 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/package.json +1 -1
package/dist/core/format.js
CHANGED
|
@@ -3,47 +3,45 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure functions for formatting prices, dates, numbers.
|
|
5
5
|
* 0 runtime dependencies — works in Node.js, Edge, Deno, Bun.
|
|
6
|
+
*
|
|
7
|
+
* Locale handling:
|
|
8
|
+
* - Every formatter takes an optional `locale: string` argument. When omitted,
|
|
9
|
+
* the runtime default (`Intl.NumberFormat().resolvedOptions().locale`) is
|
|
10
|
+
* used — in browsers this resolves from `navigator.language`, in Node from
|
|
11
|
+
* `LANG` / system settings.
|
|
12
|
+
* - For deterministic output (typical in a storefront with explicit i18n) the
|
|
13
|
+
* caller should pass `locale` explicitly (`useLocale()` in Client Components,
|
|
14
|
+
* `getLocale()` in Server Components, `next-intl`).
|
|
15
|
+
* - For per-shop locale↔currency overrides (e.g. PLN displayed in `'en-PL'`
|
|
16
|
+
* for an English-speaking customer of a Polish shop), the storefront reads
|
|
17
|
+
* `shop.localeToCurrencyMap` from the API and passes the chosen locale.
|
|
18
|
+
*
|
|
19
|
+
* Money precision:
|
|
20
|
+
* - `Money.amount` is a string (Decimal scalar) — values are forwarded to
|
|
21
|
+
* `Intl.NumberFormat.format()` verbatim, without `parseFloat`. This preserves
|
|
22
|
+
* decimal precision exactly as returned by the GraphQL API, including
|
|
23
|
+
* currencies with non-2-digit subunits (JPY/KRW, BHD/JOD, ISK, etc.).
|
|
24
|
+
*
|
|
25
|
+
* Symbol resolution:
|
|
26
|
+
* - `getCurrencySymbol(code, locale?)` derives the symbol via
|
|
27
|
+
* `Intl.NumberFormat.formatToParts()`. The SDK no longer ships a hardcoded
|
|
28
|
+
* subset of currencies — every ISO 4217 currency known to the runtime is
|
|
29
|
+
* supported, with the locale-correct symbol (`'zł'` in pl-PL, `'PLN'` in
|
|
30
|
+
* en-US, `'€'` in de-DE, etc.).
|
|
6
31
|
*/
|
|
7
32
|
// ============================================================================
|
|
8
|
-
// CONSTANTS
|
|
9
|
-
// ============================================================================
|
|
10
|
-
/** Currency symbols mapping */
|
|
11
|
-
export const CURRENCY_SYMBOLS = {
|
|
12
|
-
PLN: 'zł',
|
|
13
|
-
EUR: '€',
|
|
14
|
-
USD: '$',
|
|
15
|
-
GBP: '£',
|
|
16
|
-
CHF: 'CHF',
|
|
17
|
-
CZK: 'Kč',
|
|
18
|
-
SEK: 'kr',
|
|
19
|
-
NOK: 'kr',
|
|
20
|
-
DKK: 'kr',
|
|
21
|
-
JPY: '¥',
|
|
22
|
-
CNY: '¥',
|
|
23
|
-
AUD: 'A$',
|
|
24
|
-
CAD: 'C$',
|
|
25
|
-
};
|
|
26
|
-
/** Currency locale mapping for proper formatting */
|
|
27
|
-
export const CURRENCY_LOCALES = {
|
|
28
|
-
PLN: 'pl-PL',
|
|
29
|
-
EUR: 'de-DE',
|
|
30
|
-
USD: 'en-US',
|
|
31
|
-
GBP: 'en-GB',
|
|
32
|
-
CHF: 'de-CH',
|
|
33
|
-
CZK: 'cs-CZ',
|
|
34
|
-
SEK: 'sv-SE',
|
|
35
|
-
NOK: 'nb-NO',
|
|
36
|
-
DKK: 'da-DK',
|
|
37
|
-
JPY: 'ja-JP',
|
|
38
|
-
CNY: 'zh-CN',
|
|
39
|
-
AUD: 'en-AU',
|
|
40
|
-
CAD: 'en-CA',
|
|
41
|
-
};
|
|
42
|
-
// ============================================================================
|
|
43
33
|
// FORMATTER CACHE (avoids re-creating Intl objects on every call)
|
|
44
34
|
// ============================================================================
|
|
45
35
|
const numberFormatCache = new Map();
|
|
46
36
|
const dateFormatCache = new Map();
|
|
37
|
+
/** Runtime default locale, resolved once and reused for every `locale?`-less call. */
|
|
38
|
+
let defaultLocaleCache;
|
|
39
|
+
function resolveDefaultLocale() {
|
|
40
|
+
if (defaultLocaleCache === undefined) {
|
|
41
|
+
defaultLocaleCache = new Intl.NumberFormat().resolvedOptions().locale;
|
|
42
|
+
}
|
|
43
|
+
return defaultLocaleCache;
|
|
44
|
+
}
|
|
47
45
|
function getCurrencyFormatter(locale, currency) {
|
|
48
46
|
const key = `${locale}:${currency}`;
|
|
49
47
|
let fmt = numberFormatCache.get(key);
|
|
@@ -75,111 +73,136 @@ function getDateFormatter(locale, options) {
|
|
|
75
73
|
}
|
|
76
74
|
return fmt;
|
|
77
75
|
}
|
|
76
|
+
// Internal — forwards a Decimal string to the formatter via the string overload
|
|
77
|
+
// added through declaration merging at the top of this file. Centralised here
|
|
78
|
+
// so future precision-sensitive call sites have one place to depend on.
|
|
79
|
+
function formatDecimalString(value, formatter) {
|
|
80
|
+
return formatter.format(value);
|
|
81
|
+
}
|
|
78
82
|
// ============================================================================
|
|
79
83
|
// UTILITY FUNCTIONS
|
|
80
84
|
// ============================================================================
|
|
81
85
|
/**
|
|
82
|
-
* Get currency symbol
|
|
86
|
+
* Get the currency symbol for an ISO 4217 code in the requested locale. Derived
|
|
87
|
+
* from `Intl.NumberFormat.formatToParts()` — every currency known to the
|
|
88
|
+
* runtime is supported, with the locale-correct symbol.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* getCurrencySymbol('PLN', 'pl-PL') // => "zł"
|
|
92
|
+
* getCurrencySymbol('PLN', 'en-US') // => "PLN" (en-US has no narrow symbol for PLN)
|
|
93
|
+
* getCurrencySymbol('EUR', 'de-DE') // => "€"
|
|
94
|
+
* getCurrencySymbol('USD') // depends on the runtime locale
|
|
83
95
|
*/
|
|
84
|
-
export function getCurrencySymbol(code) {
|
|
85
|
-
|
|
96
|
+
export function getCurrencySymbol(code, locale) {
|
|
97
|
+
const resolvedLocale = locale ?? resolveDefaultLocale();
|
|
98
|
+
try {
|
|
99
|
+
const parts = new Intl.NumberFormat(resolvedLocale, {
|
|
100
|
+
style: 'currency',
|
|
101
|
+
currency: code,
|
|
102
|
+
currencyDisplay: 'symbol',
|
|
103
|
+
}).formatToParts(0);
|
|
104
|
+
return parts.find((p) => p.type === 'currency')?.value ?? code;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return code;
|
|
108
|
+
}
|
|
86
109
|
}
|
|
87
110
|
// ============================================================================
|
|
88
111
|
// PRICE FORMATTING
|
|
89
112
|
// ============================================================================
|
|
90
113
|
/**
|
|
91
|
-
* Format
|
|
114
|
+
* Format a `Money` value in the requested locale. `amount` is passed to
|
|
115
|
+
* `Intl.NumberFormat` as a string — no `parseFloat`, so decimal precision is
|
|
116
|
+
* preserved exactly as returned by the GraphQL API.
|
|
92
117
|
*
|
|
93
118
|
* @example
|
|
94
|
-
*
|
|
95
|
-
* formatPrice({ amount: "
|
|
96
|
-
* // => "
|
|
97
|
-
* ```
|
|
119
|
+
* formatPrice({ amount: "99.99", currencyCode: "USD" }, 'en-US') // => "$99.99"
|
|
120
|
+
* formatPrice({ amount: "12.50", currencyCode: "PLN" }, 'pl-PL') // => "12,50 zł"
|
|
121
|
+
* formatPrice({ amount: "115.20", currencyCode: "EUR" }, 'de-DE') // => "115,20 €"
|
|
98
122
|
*/
|
|
99
|
-
export function formatPrice(price) {
|
|
123
|
+
export function formatPrice(price, locale) {
|
|
100
124
|
if (!price)
|
|
101
125
|
return '';
|
|
102
|
-
const
|
|
103
|
-
const code = price.currencyCode;
|
|
104
|
-
const locale = CURRENCY_LOCALES[code] || 'en-US';
|
|
126
|
+
const resolvedLocale = locale ?? resolveDefaultLocale();
|
|
105
127
|
try {
|
|
106
|
-
return getCurrencyFormatter(
|
|
128
|
+
return formatDecimalString(price.amount, getCurrencyFormatter(resolvedLocale, price.currencyCode));
|
|
107
129
|
}
|
|
108
130
|
catch {
|
|
109
|
-
|
|
110
|
-
return `${amount.toFixed(2)} ${symbol}`;
|
|
131
|
+
return `${price.amount} ${price.currencyCode}`;
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
/**
|
|
114
|
-
* Format price range
|
|
135
|
+
* Format a price range (`min - max`). Returns a single price when `min` equals
|
|
136
|
+
* `max` by string comparison (no numeric conversion).
|
|
115
137
|
*
|
|
116
138
|
* @example
|
|
117
|
-
* ```typescript
|
|
118
139
|
* formatPriceRange(
|
|
119
140
|
* { amount: "10.00", currencyCode: "USD" },
|
|
120
|
-
* { amount: "50.00", currencyCode: "USD" }
|
|
141
|
+
* { amount: "50.00", currencyCode: "USD" },
|
|
142
|
+
* 'en-US'
|
|
121
143
|
* )
|
|
122
144
|
* // => "$10.00 - $50.00"
|
|
123
|
-
* ```
|
|
124
145
|
*/
|
|
125
|
-
export function formatPriceRange(minPrice, maxPrice) {
|
|
146
|
+
export function formatPriceRange(minPrice, maxPrice, locale) {
|
|
126
147
|
if (minPrice.amount === maxPrice.amount) {
|
|
127
|
-
return formatPrice(minPrice);
|
|
148
|
+
return formatPrice(minPrice, locale);
|
|
128
149
|
}
|
|
129
|
-
return `${formatPrice(minPrice)} - ${formatPrice(maxPrice)}`;
|
|
150
|
+
return `${formatPrice(minPrice, locale)} - ${formatPrice(maxPrice, locale)}`;
|
|
130
151
|
}
|
|
131
152
|
/**
|
|
132
|
-
* Format amount with currency
|
|
153
|
+
* Format an amount with currency. Accepts a string (recommended — precision
|
|
154
|
+
* preserved) or a number. String values are forwarded to `Intl.NumberFormat`
|
|
155
|
+
* verbatim; number values pass through the standard number path.
|
|
133
156
|
*
|
|
134
157
|
* @example
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* // => "115,20 €"
|
|
138
|
-
* ```
|
|
158
|
+
* formatAmount("115.20", "EUR", 'de-DE') // => "115,20 €"
|
|
159
|
+
* formatAmount(50, "USD", 'en-US') // => "$50.00"
|
|
139
160
|
*/
|
|
140
|
-
export function formatAmount(amount, currencyCode) {
|
|
141
|
-
const
|
|
142
|
-
const locale = CURRENCY_LOCALES[currencyCode] || 'en-US';
|
|
161
|
+
export function formatAmount(amount, currencyCode, locale) {
|
|
162
|
+
const resolvedLocale = locale ?? resolveDefaultLocale();
|
|
143
163
|
try {
|
|
144
|
-
|
|
164
|
+
const formatter = getCurrencyFormatter(resolvedLocale, currencyCode);
|
|
165
|
+
return typeof amount === 'string'
|
|
166
|
+
? formatDecimalString(amount, formatter)
|
|
167
|
+
: formatter.format(amount);
|
|
145
168
|
}
|
|
146
169
|
catch {
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
return typeof amount === 'string'
|
|
171
|
+
? `${amount} ${currencyCode}`
|
|
172
|
+
: `${amount.toFixed(2)} ${currencyCode}`;
|
|
149
173
|
}
|
|
150
174
|
}
|
|
151
175
|
// ============================================================================
|
|
152
176
|
// DATE FORMATTING
|
|
153
177
|
// ============================================================================
|
|
154
178
|
/**
|
|
155
|
-
* Format date
|
|
179
|
+
* Format a date in the requested locale using a `MMM d, y`-style pattern
|
|
180
|
+
* (locale-specific month abbreviation and ordering).
|
|
156
181
|
*
|
|
157
182
|
* @example
|
|
158
|
-
*
|
|
159
|
-
* formatDate(new Date())
|
|
160
|
-
* // => "
|
|
161
|
-
* ```
|
|
183
|
+
* formatDate(new Date(2025, 11, 9), 'en-US') // => "Dec 9, 2025"
|
|
184
|
+
* formatDate(new Date(2025, 11, 9), 'pl-PL') // => "9 gru 2025"
|
|
185
|
+
* formatDate('2025-12-09T12:00:00Z', 'de-DE') // => "9. Dez. 2025"
|
|
162
186
|
*/
|
|
163
|
-
export function formatDate(date) {
|
|
187
|
+
export function formatDate(date, locale) {
|
|
164
188
|
const d = typeof date === 'string' ? new Date(date) : date;
|
|
165
|
-
return getDateFormatter(
|
|
189
|
+
return getDateFormatter(locale, {
|
|
166
190
|
year: 'numeric',
|
|
167
191
|
month: 'short',
|
|
168
192
|
day: 'numeric',
|
|
169
193
|
}).format(d);
|
|
170
194
|
}
|
|
171
195
|
/**
|
|
172
|
-
* Format date with time
|
|
196
|
+
* Format a date with time in the requested locale (month-abbreviated date plus
|
|
197
|
+
* hour and minute, 12- or 24-hour depending on locale convention).
|
|
173
198
|
*
|
|
174
199
|
* @example
|
|
175
|
-
*
|
|
176
|
-
* formatDateTime(new Date())
|
|
177
|
-
* // => "Dec 9, 2025, 10:30 PM"
|
|
178
|
-
* ```
|
|
200
|
+
* formatDateTime(new Date(), 'en-US') // => "Dec 9, 2025, 10:30 PM"
|
|
201
|
+
* formatDateTime(new Date(), 'pl-PL') // => "9 gru 2025, 22:30"
|
|
179
202
|
*/
|
|
180
|
-
export function formatDateTime(date) {
|
|
203
|
+
export function formatDateTime(date, locale) {
|
|
181
204
|
const d = typeof date === 'string' ? new Date(date) : date;
|
|
182
|
-
return getDateFormatter(
|
|
205
|
+
return getDateFormatter(locale, {
|
|
183
206
|
year: 'numeric',
|
|
184
207
|
month: 'short',
|
|
185
208
|
day: 'numeric',
|
|
@@ -191,25 +214,24 @@ export function formatDateTime(date) {
|
|
|
191
214
|
// NUMBER FORMATTING
|
|
192
215
|
// ============================================================================
|
|
193
216
|
/**
|
|
194
|
-
* Format number with thousands separator
|
|
217
|
+
* Format a number with the locale-appropriate thousands separator and decimal
|
|
218
|
+
* mark.
|
|
195
219
|
*
|
|
196
220
|
* @example
|
|
197
|
-
*
|
|
198
|
-
* formatNumber(1234567)
|
|
199
|
-
* // => "1
|
|
200
|
-
* ```
|
|
221
|
+
* formatNumber(1234567, 'en-US') // => "1,234,567"
|
|
222
|
+
* formatNumber(1234567, 'pl-PL') // => "1 234 567" (NBSP separator)
|
|
223
|
+
* formatNumber(1234.5, 'de-DE') // => "1.234,5"
|
|
201
224
|
*/
|
|
202
|
-
export function formatNumber(num) {
|
|
203
|
-
return getNumberFormatter(
|
|
225
|
+
export function formatNumber(num, locale) {
|
|
226
|
+
return getNumberFormatter(locale).format(num);
|
|
204
227
|
}
|
|
205
228
|
/**
|
|
206
|
-
* Format percentage
|
|
229
|
+
* Format a 0-1 ratio as an integer percentage. Locale-independent — uses
|
|
230
|
+
* `Math.round` and a literal `%` suffix.
|
|
207
231
|
*
|
|
208
232
|
* @example
|
|
209
|
-
*
|
|
210
|
-
* formatPercentage(0.
|
|
211
|
-
* // => "15%"
|
|
212
|
-
* ```
|
|
233
|
+
* formatPercentage(0.15) // => "15%"
|
|
234
|
+
* formatPercentage(0.125) // => "13%" (Math.round half-up)
|
|
213
235
|
*/
|
|
214
236
|
export function formatPercentage(value) {
|
|
215
237
|
return `${Math.round(value * 100)}%`;
|