@godaddy/gd-commerce-storefront 0.9.0 → 0.11.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/dist/index.js CHANGED
@@ -4,6 +4,190 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as Dialog from "@radix-ui/react-dialog";
5
5
  import { Link, useParams, useSearchParams } from "react-router";
6
6
 
7
+ //#region src/messages.ts
8
+ const englishText = {
9
+ connectPrompt: "Connect a store to show products.",
10
+ connecting: "Connecting to the store…",
11
+ retryConnection: "Retry connection",
12
+ connectionSlow: "The store is taking too long to connect.",
13
+ invalidConfiguration: "The store returned invalid configuration.",
14
+ requestFailed: "Commerce request failed. Please retry.",
15
+ requestFailedStatus: "Commerce request failed ({status}). Please retry.",
16
+ requestTimeout: "Commerce information took too long to load. Please retry.",
17
+ unknownError: "Something went wrong. Please retry.",
18
+ storageUnavailable: "Your browser cannot save the cart. Keep this page open while shopping.",
19
+ invalidAddItem: "Select a product variant and a positive quantity.",
20
+ addReturnedNoCart: "Commerce did not return a cart with items. Refresh before retrying.",
21
+ itemNotInCart: "This item is no longer in the cart.",
22
+ invalidQuantity: "Quantity must be a positive whole number.",
23
+ updateReturnedNoCart: "Commerce did not return the updated cart. Refresh before retrying.",
24
+ discountMissing: "Add an item and enter a promotion code.",
25
+ discountReturnedNoCart: "Commerce did not return the updated cart.",
26
+ checkoutDisabled: "Checkout has not been enabled for this store.",
27
+ checkoutEmpty: "Add an item before checking out.",
28
+ checkoutNoUrl: "Commerce did not return a checkout URL.",
29
+ checkoutInvalidUrl: "Commerce returned an invalid checkout URL.",
30
+ itemAdded: "{name} added to your cart.",
31
+ sampleItemAdded: "{name} added to your sample cart.",
32
+ itemRemoved: "Item removed from cart.",
33
+ quantityUpdated: "Cart quantity updated.",
34
+ discountsUpdated: "Promotion codes updated.",
35
+ sampleQuantityUpdated: "Sample cart quantity updated.",
36
+ sampleItemRemoved: "Sample item removed from cart.",
37
+ cartButtonLabel: {
38
+ one: "Cart, {count} item",
39
+ other: "Cart, {count} items"
40
+ },
41
+ addToCart: "Add to cart",
42
+ addToCartWithPrice: "Add to cart · {price}",
43
+ addToCartWithPriceName: "Add to cart, {price}",
44
+ adding: "Adding…",
45
+ outOfStock: "Out of stock",
46
+ addReasonUnbound: "Connect a store to add items to the cart.",
47
+ addReasonError: "The store could not be reached.",
48
+ addReasonNoSku: "This item cannot be added to the cart.",
49
+ loadingCart: "Loading your cart…",
50
+ quantityNotWhole: "Enter a whole number of 1 or more.",
51
+ quantityOverStock: "Only {count} available.",
52
+ cartTitle: "Your cart",
53
+ sampleCartTitle: "Sample cart",
54
+ cartDescription: "Review your items before checkout.",
55
+ sampleCartDescription: "Example products and prices in {currency}. Nothing can be purchased.",
56
+ closeCart: "Close cart",
57
+ refreshCart: "Refresh cart",
58
+ priceUnavailable: "Price unavailable",
59
+ decreaseQuantity: "Decrease quantity of {name}",
60
+ increaseQuantity: "Increase quantity of {name}",
61
+ quantityPrefix: "Quantity: ",
62
+ remove: "Remove",
63
+ itemOption: "{attribute}: {values}",
64
+ cartEmpty: "Your cart is empty",
65
+ cartEmptyHint: "Add something you love and it will show up here.",
66
+ sampleCartEmptyHint: "Add a sample product to try the cart.",
67
+ browseProducts: "Browse products",
68
+ subtotal: "Subtotal",
69
+ sampleSubtotal: "Sample subtotal",
70
+ checkoutAdjustments: "Shipping, taxes & discounts",
71
+ checkoutAdjustmentsValue: "Calculated at checkout",
72
+ sampleCheckoutNote: "Example total in {currency}. Checkout is not set up yet.",
73
+ checkoutUnavailable: "Checkout is not available yet.",
74
+ checkout: "Proceed to checkout",
75
+ openingCheckout: "Opening checkout…",
76
+ continueShopping: "Continue shopping",
77
+ catalogTitle: "Shop all products",
78
+ catalogDescription: "Explore the collection and find your favorites.",
79
+ sampleCatalogSummary: "Sample products · Example prices in {currency}",
80
+ loadingProducts: "Loading products…",
81
+ productsUnavailable: "Products unavailable",
82
+ productCount: {
83
+ one: "Showing {count} product",
84
+ other: "Showing {count} products"
85
+ },
86
+ retryProducts: "Retry products",
87
+ noProducts: "No products available.",
88
+ productPages: "Product pages",
89
+ firstPage: "First page",
90
+ nextPage: "Next page",
91
+ sampleProductName: "Product {number}",
92
+ relatedTitle: "You may also like",
93
+ imageUnavailable: "{name}: image unavailable",
94
+ noImage: "No image available",
95
+ sale: "Sale",
96
+ wasPrice: "Was",
97
+ swatchSummary: {
98
+ one: "{count} {attribute} option: {values}",
99
+ other: "{count} {attribute} options: {values}"
100
+ },
101
+ chooseOptions: "Choose options",
102
+ viewProduct: "View product",
103
+ addSampleToCart: "Add {name} to sample cart",
104
+ sampleItemName: "({name}, sample)",
105
+ loadingProduct: "Loading product…",
106
+ productUnavailable: "Product unavailable",
107
+ retryProduct: "Retry product",
108
+ productNotFound: "Product not found",
109
+ backToShop: "Back to shop",
110
+ viewImage: "View image {number}",
111
+ priceFrom: "From {price}",
112
+ selectAllOptions: "Select all options to add this item to your cart.",
113
+ checkingVariant: "Checking this variant…",
114
+ variantCheckFailed: "Availability for this selection could not be checked.",
115
+ combinationUnavailable: "This combination is unavailable. Choose different options.",
116
+ notPurchasable: "This product is not currently available to purchase.",
117
+ selectAttribute: "Select {attribute}",
118
+ selectAttributeError: "Select {attribute}.",
119
+ selectOption: "Select an option",
120
+ skuOptionLabel: "Option",
121
+ soldOutOption: "{label} (sold out)",
122
+ soldOutChoice: "{label}, sold out",
123
+ retryAvailability: "Retry availability",
124
+ availableToOrder: "Available to order",
125
+ variantOutOfStock: "This variant is out of stock",
126
+ availableCount: "{count} available",
127
+ quantity: "Quantity"
128
+ };
129
+ /** The package's English text, the default for every key a site does not set. */
130
+ const enUS = {
131
+ locale: "en-US",
132
+ errorCodes: {},
133
+ ...englishText
134
+ };
135
+ /** `key` from `messages`, with its plural form chosen by `values.count` and its placeholders filled in. */
136
+ function formatMessage(messages, key, values = {}) {
137
+ return (pickText(messages[key], values.count, messages.locale) ?? pickText(enUS[key], values.count, enUS.locale) ?? key).replace(/\{(\w+)\}/g, (placeholder, name) => {
138
+ const value = values[name];
139
+ if (value === void 0) return placeholder;
140
+ return typeof value === "number" ? numberFormat(messages.locale).format(value) : value;
141
+ });
142
+ }
143
+ function pickText(message, count, locale) {
144
+ if (typeof message === "string") return message;
145
+ if (typeof message !== "object" || message === null) return void 0;
146
+ const forms = message;
147
+ const form = typeof count === "number" ? pluralRules(locale).select(count) : void 0;
148
+ const chosen = form && forms[form];
149
+ if (typeof chosen === "string" && chosen) return chosen;
150
+ return typeof forms.other === "string" ? forms.other : void 0;
151
+ }
152
+ /** Whether the text is English, the language of the server's own error messages. */
153
+ const isEnglish = (messages) => /^en(?:-|$)/i.test(messages.locale);
154
+ /** An error whose text is a package message, so it shows in the provider's language. */
155
+ var StorefrontError = class extends Error {
156
+ constructor(key, values = {}, options) {
157
+ super(formatMessage(enUS, key, values), options);
158
+ this.key = key;
159
+ this.values = values;
160
+ this.name = "StorefrontError";
161
+ }
162
+ };
163
+ const pluralRulesCache = /* @__PURE__ */ new Map();
164
+ const numberFormatCache = /* @__PURE__ */ new Map();
165
+ function pluralRules(locale) {
166
+ let rules = pluralRulesCache.get(locale);
167
+ if (!rules) {
168
+ rules = new Intl.PluralRules(safeLocale(locale));
169
+ pluralRulesCache.set(locale, rules);
170
+ }
171
+ return rules;
172
+ }
173
+ function numberFormat(locale) {
174
+ let format = numberFormatCache.get(locale);
175
+ if (!format) {
176
+ format = new Intl.NumberFormat(safeLocale(locale));
177
+ numberFormatCache.set(locale, format);
178
+ }
179
+ return format;
180
+ }
181
+ /** `locale`, or `en-US` when it is not a valid tag, so a typo in a message file cannot break rendering. */
182
+ function safeLocale(locale) {
183
+ try {
184
+ return Intl.getCanonicalLocales(locale)[0] ?? "en-US";
185
+ } catch {
186
+ return "en-US";
187
+ }
188
+ }
189
+
190
+ //#endregion
7
191
  //#region src/api.ts
8
192
  var CartIdStorage = class {
9
193
  currentId = null;
@@ -24,9 +208,15 @@ var CartIdStorage = class {
24
208
  }
25
209
  };
26
210
  var ApiError = class extends Error {
27
- constructor(message$1, status, options) {
28
- super(message$1, options);
211
+ /** The failure response's `code`, such as `not_found`, when the server sent one. */
212
+ code;
213
+ /** Package text to show in place of `message`, for failures the package describes itself. */
214
+ text;
215
+ constructor(message, status, options) {
216
+ super(message, options);
29
217
  this.status = status;
218
+ this.code = options?.code;
219
+ this.text = options?.text;
30
220
  }
31
221
  };
32
222
  const checkedFetch = async (input, init) => {
@@ -37,10 +227,23 @@ const checkedFetch = async (input, init) => {
37
227
  try {
38
228
  return JSON.parse(data);
39
229
  } catch (cause) {
40
- throw new ApiError(`Commerce request failed (${response.status}). Please retry.`, response.status, { cause });
230
+ const text = {
231
+ key: "requestFailedStatus",
232
+ values: { status: response.status }
233
+ };
234
+ throw new ApiError(formatMessage(enUS, text.key, text.values), response.status, {
235
+ cause,
236
+ text
237
+ });
41
238
  }
42
239
  })();
43
- throw new ApiError(typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "string" ? parsed.error : "Commerce request failed. Please retry.", response.status);
240
+ const body = typeof parsed === "object" && parsed !== null ? { ...parsed } : {};
241
+ const code = typeof body.code === "string" ? body.code : void 0;
242
+ if (typeof body.error === "string") throw new ApiError(body.error, response.status, { code });
243
+ throw new ApiError(formatMessage(enUS, "requestFailed"), response.status, {
244
+ code,
245
+ text: { key: "requestFailed" }
246
+ });
44
247
  }
45
248
  return response;
46
249
  };
@@ -57,22 +260,65 @@ async function request(path, init) {
57
260
  cache: "no-store"
58
261
  })).json();
59
262
  } catch (cause) {
60
- if (timeout?.aborted && !init?.signal?.aborted) throw new ApiError("Commerce information took too long to load. Please retry.", 408, { cause });
263
+ if (timeout?.aborted && !init?.signal?.aborted) throw new ApiError(formatMessage(enUS, "requestTimeout"), 408, {
264
+ cause,
265
+ text: { key: "requestTimeout" }
266
+ });
61
267
  throw cause;
62
268
  }
63
269
  }
270
+ const currencyFormats = /* @__PURE__ */ new Map();
271
+ /** A page uses a few pairs; the limit bounds a long-running server rendering many locales and currencies. */
272
+ const maxCurrencyFormats = 64;
273
+ function currencyFormat(currency, locale) {
274
+ const key = `${locale}|${currency}`;
275
+ let cached = currencyFormats.get(key);
276
+ if (!cached) {
277
+ const format = new Intl.NumberFormat(locale, {
278
+ style: "currency",
279
+ currency
280
+ });
281
+ cached = {
282
+ format,
283
+ scale: 10 ** (format.resolvedOptions().maximumFractionDigits ?? 2)
284
+ };
285
+ if (currencyFormats.size >= maxCurrencyFormats) currencyFormats.delete(currencyFormats.keys().next().value ?? "");
286
+ currencyFormats.set(key, cached);
287
+ }
288
+ return cached;
289
+ }
64
290
  /** Formats minor units, such as cents, as currency. Inside the provider, prefer `useMoney`, which follows its `locale`. */
65
291
  function money(value, currency, locale = "en-US") {
66
- const formatter = new Intl.NumberFormat(locale, {
67
- style: "currency",
68
- currency
69
- });
70
- const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
71
- return formatter.format(value / 10 ** digits);
292
+ const { format, scale } = currencyFormat(currency, locale);
293
+ return format.format(value / scale);
72
294
  }
73
- function message(error) {
74
- return error instanceof Error ? error.message : "Something went wrong. Please retry.";
295
+ /** Formats a range of minor units the way `locale` writes ranges, such as `$20.00 – $25.00` or `20,00–25,00 €`. */
296
+ function moneyRange(min, max, currency, locale = "en-US") {
297
+ const { format, scale } = currencyFormat(currency, locale);
298
+ if (min === max) return format.format(min / scale);
299
+ try {
300
+ if (typeof format.formatRange === "function") return format.formatRange(min / scale, max / scale);
301
+ } catch {}
302
+ return `${format.format(min / scale)} – ${format.format(max / scale)}`;
75
303
  }
304
+ /**
305
+ * The text to show for a failure, in the language of `messages`. Server messages are English, so other languages
306
+ * show the text for the response's `code`, or a generic message.
307
+ */
308
+ function describeError(error, messages) {
309
+ const codes = messages.errorCodes;
310
+ const coded = error instanceof ApiError && error.code && codes && Object.hasOwn(codes, error.code) ? codes[error.code] : void 0;
311
+ if (typeof coded === "string" && coded) return coded;
312
+ const text = error instanceof StorefrontError || error instanceof ApiError ? textOf(error) : void 0;
313
+ if (text) return formatMessage(messages, text.key, text.values);
314
+ if (error instanceof ApiError) return isEnglish(messages) ? error.message : formatMessage(messages, "requestFailed");
315
+ if (error instanceof Error && isEnglish(messages)) return error.message;
316
+ return formatMessage(messages, "unknownError");
317
+ }
318
+ const textOf = (error) => error instanceof StorefrontError ? {
319
+ key: error.key,
320
+ values: error.values
321
+ } : error.text;
76
322
 
77
323
  //#endregion
78
324
  //#region src/cart-model.ts
@@ -85,9 +331,10 @@ async function addToCart(cartId, item, fetchFn = globalThis.fetch) {
85
331
  body: JSON.stringify(body)
86
332
  })).json();
87
333
  }
88
- function getCartSummaryTotals(order) {
334
+ /** The cart's totals in minor units. `currencyCode` is the cart's, or `fallbackCurrency`, such as the store's. */
335
+ function getCartSummaryTotals(order, fallbackCurrency = "USD") {
89
336
  const lineItems = order?.lineItems ?? [];
90
- const currencyCode = order?.totals?.total?.currencyCode || "USD";
337
+ const currencyCode = order?.totals?.total?.currencyCode || fallbackCurrency;
91
338
  return {
92
339
  itemCount: lineItems.reduce((sum, item) => sum + (item.quantity || 0), 0),
93
340
  currencyCode,
@@ -101,54 +348,130 @@ function getCartSummaryTotals(order) {
101
348
 
102
349
  //#endregion
103
350
  //#region src/sample-cart.tsx
104
- const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
351
+ /** The six packaged samples, "Product 1" to "Product 6", without images. */
352
+ const builtInSamples = Array.from({ length: 6 }, (_, index) => ({
105
353
  key: `sample-${index + 1}`,
106
354
  name: `Product ${index + 1}`,
107
355
  price: (index + 1) * 1e3
108
356
  }));
109
- const noQuantities = {};
357
+ const builtInNumbers = new Map(builtInSamples.map((sample, index) => [sample.key, index + 1]));
358
+ const builtInNumber = (sample) => builtInNumbers.get(sample.key);
359
+ const isBuiltInSample = (sample) => builtInNumber(sample) !== void 0;
360
+ /** Sample prices are examples, in this currency whatever the store's. */
361
+ const sampleCurrency = "USD";
362
+ /** A sample's name. Packaged samples are named in the provider's language, such as "Product 1". */
363
+ const sampleName = (text, sample) => {
364
+ const number = builtInNumber(sample);
365
+ return number === void 0 ? sample.name : text("sampleProductName", { number });
366
+ };
367
+ const validPrice = (price) => Number.isSafeInteger(price) && price >= 0;
368
+ /** The first reason host samples could not be shown or added consistently, or `null` when they can. */
369
+ function sampleProblem(samples) {
370
+ if (!samples.length) return "CatalogRoot sampleProducts needs at least one product, or `true`.";
371
+ const keys = /* @__PURE__ */ new Set();
372
+ for (const [index, sample] of samples.entries()) {
373
+ if (typeof sample !== "object" || sample === null) return `CatalogRoot sampleProducts: entry ${index} is ${String(sample)}, not a sample.`;
374
+ if (typeof sample.key !== "string" || !sample.key || keys.has(sample.key)) return `CatalogRoot sampleProducts need unique, non-empty string keys, not "${String(sample.key)}".`;
375
+ if (builtInNumbers.has(sample.key)) return `CatalogRoot sampleProducts: "${sample.key}" is a packaged sample's key. Use another key.`;
376
+ keys.add(sample.key);
377
+ if (typeof sample.name !== "string" || !sample.name.trim()) return `CatalogRoot sampleProducts: "${sample.key}" needs a non-empty name.`;
378
+ for (const field of ["description", "imageUrl"]) if (sample[field] !== void 0 && typeof sample[field] !== "string") return `CatalogRoot sampleProducts: "${sample.key}" has a ${field} that is not a string.`;
379
+ for (const field of ["price", "compareAtPrice"]) {
380
+ const value = sample[field];
381
+ if ((field === "price" || value !== void 0) && !validPrice(value)) return `CatalogRoot sampleProducts: "${sample.key}" has an invalid ${field}, ${String(value)}. A price must be a whole number of minor units, 0 or more.`;
382
+ }
383
+ }
384
+ return null;
385
+ }
386
+ const sameSample = (a, b) => {
387
+ if (a === b) return true;
388
+ if (typeof a !== "object" || !a || typeof b !== "object" || !b) return false;
389
+ const fields = Object.keys(a);
390
+ return fields.length === Object.keys(b).length && fields.every((field) => Object.is(a[field], b[field]));
391
+ };
392
+ /** Whether two sample lists have the same samples with the same field values, so an inline list can be reused. */
393
+ const sameSamples = (a, b) => a === b || a.length === b.length && a.every((sample, index) => sameSample(sample, b[index]));
394
+ const noLines = [];
110
395
  const SampleCartContext = createContext(null);
111
396
  function useSampleCart() {
112
397
  const value = useContext(SampleCartContext);
113
398
  if (!value) throw new Error("Mount CommerceProvider around sample products.");
114
399
  return value;
115
400
  }
116
- function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
401
+ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children }) {
402
+ const text = useCallback((key, values) => formatMessage(messages, key, values), [messages]);
117
403
  const session = useMemo(() => ({ enabled }), [enabled]);
118
404
  const active = useRef(session);
119
405
  active.current = session;
120
406
  const [state, setState] = useState({
121
407
  session,
122
- quantities: {}
408
+ lines: noLines
123
409
  });
124
410
  useEffect(() => {
125
411
  active.current = session;
126
412
  setState({
127
413
  session,
128
- quantities: {}
414
+ lines: noLines
129
415
  });
130
416
  return () => {
131
417
  active.current = null;
132
418
  };
133
419
  }, [session]);
134
- const quantities = state.session === session && enabled ? state.quantities : noQuantities;
135
- const items = useMemo(() => sampleProducts.flatMap((product) => quantities[product.key] ? [{
136
- ...product,
137
- quantity: quantities[product.key]
138
- }] : []), [quantities]);
139
- function update(key, quantity) {
140
- if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
420
+ const lines = state.session === session && enabled ? state.lines : noLines;
421
+ const items = useMemo(() => [...lines].sort((a, b) => (builtInNumber(a.sample) ?? Number.POSITIVE_INFINITY) - (builtInNumber(b.sample) ?? Number.POSITIVE_INFINITY)).map(({ sample, quantity }) => ({
422
+ key: sample.key,
423
+ name: sampleName(text, sample),
424
+ price: sample.price,
425
+ quantity,
426
+ sample
427
+ })), [lines, text]);
428
+ const refresh = useCallback((samples) => {
429
+ const byKey = new Map(samples.map((sample) => [sample.key, sample]));
430
+ setState((previous) => {
431
+ if (previous.session !== session) return previous;
432
+ let changed = false;
433
+ const next = previous.lines.map((line) => {
434
+ const current = byKey.get(line.sample.key);
435
+ if (!current || current === line.sample) return line;
436
+ changed = true;
437
+ return {
438
+ ...line,
439
+ sample: current
440
+ };
441
+ });
442
+ return changed ? {
443
+ session,
444
+ lines: next
445
+ } : previous;
446
+ });
447
+ }, [session]);
448
+ function update(key, quantity, added) {
449
+ if (!enabled || active.current !== session) return false;
450
+ if (!added && !lines.some((line) => line.sample.key === key)) return false;
141
451
  if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
142
452
  setState((previous) => {
143
- const current = previous.session === session ? previous.quantities : {};
144
- const next = quantity ?? (current[key] ?? 0) + 1;
453
+ const current = previous.session === session ? previous.lines : noLines;
454
+ const line = current.find((candidate) => candidate.sample.key === key);
455
+ const next = quantity ?? (line?.quantity ?? 0) + 1;
145
456
  if (next > 999) return previous;
457
+ const kept = current.filter((candidate) => candidate !== line);
458
+ if (next === 0) return {
459
+ session,
460
+ lines: kept
461
+ };
462
+ if (!line && added) return {
463
+ session,
464
+ lines: [...current, {
465
+ sample: added,
466
+ quantity: next
467
+ }]
468
+ };
146
469
  return {
147
470
  session,
148
- quantities: {
149
- ...current,
150
- [key]: next
151
- }
471
+ lines: current.map((candidate) => candidate === line ? {
472
+ ...candidate,
473
+ quantity: next
474
+ } : candidate)
152
475
  };
153
476
  });
154
477
  return true;
@@ -156,16 +479,16 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
156
479
  return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
157
480
  value: {
158
481
  items,
159
- add: (key) => {
160
- const product = sampleProducts.find((product$1) => product$1.key === key);
161
- if (!product || !update(key)) return false;
162
- onAdd(product.name);
482
+ refresh,
483
+ add: (sample) => {
484
+ if (!update(sample.key, void 0, sample)) return false;
485
+ onAdd(sampleName(text, sample));
163
486
  return true;
164
487
  },
165
488
  changeQuantity: async (key, quantity) => {
166
489
  const updated = quantity >= 1 && update(key, quantity);
167
490
  if (updated) {
168
- announce("Sample cart quantity updated.");
491
+ announce("sampleQuantityUpdated");
169
492
  emit({
170
493
  type: "quantity-changed",
171
494
  itemId: key,
@@ -178,7 +501,7 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
178
501
  remove: async (key) => {
179
502
  const updated = update(key, 0);
180
503
  if (updated) {
181
- announce("Sample item removed from cart.");
504
+ announce("sampleItemRemoved");
182
505
  emit({
183
506
  type: "item-removed",
184
507
  itemId: key,
@@ -211,12 +534,50 @@ function useMoney() {
211
534
  const { currencyCode } = config;
212
535
  return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
213
536
  }
537
+ /** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
538
+ function useMoneyRange() {
539
+ const { locale, config } = useCommerce();
540
+ const { currencyCode } = config;
541
+ return useCallback((min, max, currency = currencyCode) => moneyRange(min, max, currency, locale), [locale, currencyCode]);
542
+ }
543
+ /**
544
+ * The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
545
+ * it. For custom parts that should match the package's language. English outside a provider.
546
+ */
547
+ function useMessage() {
548
+ const messages = useContext(CommerceContext)?.messages ?? enUS;
549
+ return useCallback((key, values) => formatMessage(messages, key, values), [messages]);
550
+ }
214
551
  const CONNECTING_FAILURE_GRACE_MS = 6e4;
215
552
  /** Polls every five seconds for five minutes after the page loads, then every minute, and stops at thirty. */
216
553
  const FAST_POLL_FOR_MS = 5 * 6e4;
217
554
  const STOP_POLLING_AT_MS = 30 * 6e4;
218
555
  /** A failed retry keeps showing connecting this long so the click visibly does something. */
219
556
  const RETRY_FEEDBACK_MS = 1e3;
557
+ /**
558
+ * `messages` while its contents are unchanged, so an inline object, including inline `errorCodes` or plural forms,
559
+ * does not change every render. The kept value is state, so a render React discards cannot leave it behind.
560
+ */
561
+ function useStableMessages(value) {
562
+ const [stable, setStable] = useState(value);
563
+ if (stable === value || sameMessages(stable, value)) return stable;
564
+ setStable(() => value);
565
+ return value;
566
+ }
567
+ /** Equal keys and values, comparing plain-object values, such as `errorCodes` and plural forms, one level deeper. */
568
+ function sameMessages(a, b, deeper = true) {
569
+ if (a === b) return true;
570
+ if (!a || !b) return false;
571
+ const left = a;
572
+ const right = b;
573
+ const keys = Object.keys(left);
574
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && (Object.is(left[key], right[key]) || deeper && isPlainObject(left[key]) && isPlainObject(right[key]) && sameMessages(left[key], right[key], false)));
575
+ }
576
+ function isPlainObject(value) {
577
+ if (typeof value !== "object" || value === null) return false;
578
+ const prototype = Object.getPrototypeOf(value);
579
+ return prototype === Object.prototype || prototype === null;
580
+ }
220
581
  /** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
221
582
  function resolveLocale(locale) {
222
583
  if (!locale) return "en-US";
@@ -243,18 +604,28 @@ function callHost(callback) {
243
604
  async function loadConfig(signal) {
244
605
  const config = await request("/config", { signal });
245
606
  if (config?.state === "unbound" || config?.state === "connecting") {
246
- if ("cartScope" in config || "currencyCode" in config) throw new Error("The store returned invalid configuration.");
607
+ if ("cartScope" in config || "currencyCode" in config) throw new StorefrontError("invalidConfiguration");
247
608
  return { state: config.state };
248
609
  }
249
- if (!config || config.state !== void 0 && config.state !== "ready" || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new Error("The store returned invalid configuration.");
610
+ if (!config || config.state !== void 0 && config.state !== "ready" || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new StorefrontError("invalidConfiguration");
250
611
  return {
251
612
  state: "ready",
252
613
  cartScope: config.cartScope,
253
614
  currencyCode: config.currencyCode
254
615
  };
255
616
  }
256
- function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
617
+ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, messages, onItemAdded, onEvent }) {
257
618
  const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
619
+ const stableMessages = useStableMessages(messages);
620
+ const resolvedMessages = useMemo(() => ({
621
+ ...enUS,
622
+ ...stableMessages,
623
+ locale: stableMessages?.locale || enUS.locale,
624
+ errorCodes: {
625
+ ...enUS.errorCodes,
626
+ ...stableMessages?.errorCodes
627
+ }
628
+ }), [stableMessages]);
258
629
  const [pollingSince, setPollingSince] = useState(Date.now);
259
630
  const [pollingStopped, setPollingStopped] = useState(false);
260
631
  const [retrying, setRetrying] = useState(false);
@@ -300,7 +671,7 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
300
671
  checkoutSuccessPath
301
672
  },
302
673
  connection,
303
- connectionError: connection !== "error" ? null : stalled ? "The store is taking too long to connect." : message(config.error),
674
+ connectionError: connection !== "error" ? null : stalled ? formatMessage(resolvedMessages, "connectionSlow") : describeError(config.error, resolvedMessages),
304
675
  retryConnection: () => {
305
676
  const started = Date.now();
306
677
  setPollingSince(started);
@@ -317,12 +688,14 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
317
688
  theme,
318
689
  getSwatch,
319
690
  locale: resolvedLocale,
691
+ messages: resolvedMessages,
320
692
  onItemAdded,
321
693
  onEvent,
322
694
  children
323
695
  });
324
696
  }
325
- function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
697
+ function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, messages, onItemAdded, onEvent }) {
698
+ const text = (key, values) => formatMessage(messages, key, values);
326
699
  const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
327
700
  const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
328
701
  const ready = connection === "ready";
@@ -338,8 +711,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
338
711
  const [open, setOpen] = useState(false);
339
712
  const [pending, setPending] = useState(false);
340
713
  const [hydrating, setHydrating] = useState(true);
341
- const [error, setError] = useState(null);
342
- const [storageWarning, setStorageWarning] = useState(null);
714
+ const [failure, setError] = useState(null);
715
+ const [storageUnavailable, setStorageUnavailable] = useState(false);
716
+ const error = failure && describeError(failure.cause, messages);
717
+ const storageWarning = storageUnavailable ? text("storageUnavailable") : null;
343
718
  const [announcement, setAnnouncement] = useState("");
344
719
  const id = useRef(null);
345
720
  const currentCart = useRef(null);
@@ -399,7 +774,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
399
774
  try {
400
775
  return cartIdStorage.read();
401
776
  } catch {
402
- setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
777
+ setStorageUnavailable(true);
403
778
  return id.current;
404
779
  }
405
780
  }
@@ -410,9 +785,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
410
785
  setCart(next);
411
786
  try {
412
787
  cartIdStorage.write(id.current);
413
- setStorageWarning(null);
788
+ setStorageUnavailable(false);
414
789
  } catch {
415
- setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
790
+ setStorageUnavailable(true);
416
791
  }
417
792
  }
418
793
  async function hydrate() {
@@ -446,12 +821,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
446
821
  return isActive();
447
822
  } catch (cause) {
448
823
  if (isActive()) {
449
- const text = message(cause);
450
- setError(text);
824
+ const shown = describeError(cause, messages);
825
+ setError({ cause });
826
+ const code = cause instanceof ApiError ? cause.code : void 0;
451
827
  emit({
452
828
  type: "error",
453
829
  operation: kind,
454
- message: text
830
+ message: shown,
831
+ ...code ? { code } : {}
455
832
  });
456
833
  }
457
834
  return false;
@@ -476,7 +853,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
476
853
  setCart(null);
477
854
  setOpen(false);
478
855
  setError(null);
479
- setStorageWarning(null);
856
+ setStorageUnavailable(false);
480
857
  setAnnouncement("");
481
858
  setPending(false);
482
859
  setHydrating(ready);
@@ -498,7 +875,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
498
875
  function addItem(item) {
499
876
  rememberOpener();
500
877
  return run("add", async () => {
501
- if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new Error("Select a product variant and a positive quantity.");
878
+ if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new StorefrontError("invalidAddItem");
502
879
  await hydrate();
503
880
  if (!isActive()) return;
504
881
  const fetchCart = (input, init) => {
@@ -514,10 +891,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
514
891
  name: item.name,
515
892
  quantity: item.quantity
516
893
  }, fetchCart);
517
- if (!result.cart?.id || !result.cart.lineItems?.length) throw new Error("Commerce did not return a cart with items. Refresh before retrying.");
894
+ if (!result.cart?.id || !result.cart.lineItems?.length) throw new StorefrontError("addReturnedNoCart");
518
895
  if (!isActive()) return;
519
896
  commit(result.cart);
520
- setAnnouncement(`${item.name} added to your cart.`);
897
+ setAnnouncement(text("itemAdded", { name: item.name }));
521
898
  openDrawer();
522
899
  emit({
523
900
  type: "item-added",
@@ -532,16 +909,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
532
909
  return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
533
910
  await hydrate();
534
911
  if (!isActive()) return;
535
- if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new Error("This item is no longer in the cart.");
536
- if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new Error("Quantity must be a positive whole number.");
912
+ if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new StorefrontError("itemNotInCart");
913
+ if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new StorefrontError("invalidQuantity");
537
914
  const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/items/${encodeURIComponent(itemId)}`, {
538
915
  method,
539
916
  ...quantity !== void 0 ? { body: JSON.stringify({ quantity }) } : {}
540
917
  });
541
- if (!result.cart?.id) throw new Error("Commerce did not return the updated cart. Refresh before retrying.");
918
+ if (!result.cart?.id) throw new StorefrontError("updateReturnedNoCart");
542
919
  if (!isActive()) return;
543
920
  commit(result.cart);
544
- setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
921
+ setAnnouncement(text(method === "DELETE" ? "itemRemoved" : "quantityUpdated"));
545
922
  emit(method === "DELETE" ? {
546
923
  type: "item-removed",
547
924
  itemId,
@@ -558,16 +935,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
558
935
  return run("discount", async () => {
559
936
  await hydrate();
560
937
  if (!isActive()) return;
561
- if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
938
+ if (!id.current || !code.trim()) throw new StorefrontError("discountMissing");
562
939
  const codes = [...new Set([...currentCart.current?.discounts?.flatMap((discount) => discount.code ? [discount.code] : []) ?? [], code.trim()])];
563
940
  const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/discounts`, {
564
941
  method: "POST",
565
942
  body: JSON.stringify({ discountCodes: codes })
566
943
  });
567
- if (!result.cart?.id) throw new Error("Commerce did not return the updated cart.");
944
+ if (!result.cart?.id) throw new StorefrontError("discountReturnedNoCart");
568
945
  if (!isActive()) return;
569
946
  commit(result.cart);
570
- setAnnouncement("Promotion codes updated.");
947
+ setAnnouncement(text("discountsUpdated"));
571
948
  emit({
572
949
  type: "discount-applied",
573
950
  code: code.trim()
@@ -576,10 +953,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
576
953
  }
577
954
  function checkout() {
578
955
  return run("checkout", async () => {
579
- if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
956
+ if (!config.checkoutSuccessPath) throw new StorefrontError("checkoutDisabled");
580
957
  await hydrate();
581
958
  if (!isActive()) return;
582
- if (!id.current || !currentCart.current?.lineItems?.length) throw new Error("Add an item before checking out.");
959
+ if (!id.current || !currentCart.current?.lineItems?.length) throw new StorefrontError("checkoutEmpty");
583
960
  const origin = window.location.origin;
584
961
  const successUrl = new URL(config.checkoutSuccessPath, origin);
585
962
  successUrl.searchParams.set("orderId", id.current);
@@ -592,9 +969,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
592
969
  })
593
970
  });
594
971
  if (!isActive()) return;
595
- if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
972
+ if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new StorefrontError("checkoutNoUrl");
596
973
  const url = new URL(session$1.url, origin);
597
- if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
974
+ if (url.protocol !== "https:") throw new StorefrontError("checkoutInvalidUrl");
598
975
  emit({
599
976
  type: "checkout-started",
600
977
  cartId: id.current
@@ -611,6 +988,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
611
988
  theme,
612
989
  getSwatch,
613
990
  locale,
991
+ messages,
614
992
  cart,
615
993
  open,
616
994
  setOpen: (next) => {
@@ -637,10 +1015,11 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
637
1015
  value: registerDrawer,
638
1016
  children: /* @__PURE__ */ jsx(SampleCartProvider, {
639
1017
  enabled: connection === "unbound",
640
- announce: setAnnouncement,
1018
+ messages,
1019
+ announce: (key) => setAnnouncement(text(key)),
641
1020
  emit,
642
1021
  onAdd: (name) => {
643
- setAnnouncement(`${name} added to your sample cart.`);
1022
+ setAnnouncement(text("sampleItemAdded", { name }));
644
1023
  openDrawer();
645
1024
  emit({
646
1025
  type: "item-added",
@@ -668,21 +1047,22 @@ function useCart() {
668
1047
  const { cart, config, connection, hydrating } = useCommerce();
669
1048
  const sample = useSampleCart();
670
1049
  const isSample = connection === "unbound";
671
- const sampleItems$1 = sample.items;
1050
+ const sampleItems = sample.items;
672
1051
  return useMemo(() => {
673
1052
  if (isSample) return {
674
- items: sampleItems$1.map((item) => ({
1053
+ items: sampleItems.map((item) => ({
675
1054
  id: item.key,
676
1055
  name: item.name,
677
1056
  quantity: item.quantity,
678
- imageUrl: null,
1057
+ imageUrl: item.sample.imageUrl || null,
679
1058
  options: [],
680
1059
  subtotal: item.price * item.quantity,
681
- currency: "USD"
1060
+ currency: sampleCurrency,
1061
+ sample: item.sample
682
1062
  })),
683
- itemCount: sampleItems$1.reduce((total, item) => total + item.quantity, 0),
684
- subtotal: sampleItems$1.reduce((sum, item) => sum + item.price * item.quantity, 0),
685
- currency: "USD",
1063
+ itemCount: sampleItems.reduce((total, item) => total + item.quantity, 0),
1064
+ subtotal: sampleItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
1065
+ currency: sampleCurrency,
686
1066
  isSample,
687
1067
  loading: false
688
1068
  };
@@ -700,7 +1080,8 @@ function useCart() {
700
1080
  values: option.values ?? []
701
1081
  })),
702
1082
  subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
703
- currency: item.totals?.subTotal?.currencyCode ?? currency
1083
+ currency: item.totals?.subTotal?.currencyCode ?? currency,
1084
+ sample: null
704
1085
  })),
705
1086
  itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
706
1087
  subtotal: typeof subtotal === "number" ? subtotal : null,
@@ -713,7 +1094,7 @@ function useCart() {
713
1094
  config.currencyCode,
714
1095
  hydrating,
715
1096
  isSample,
716
- sampleItems$1
1097
+ sampleItems
717
1098
  ]);
718
1099
  }
719
1100
 
@@ -907,15 +1288,16 @@ function StorefrontSurface({ children, className = "" }) {
907
1288
  }
908
1289
  function CommerceStatus() {
909
1290
  const { connection, connectionError, retryConnection } = useCommerce();
1291
+ const text = useMessage();
910
1292
  if (connection === "ready") return null;
911
1293
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
912
1294
  role: "status",
913
1295
  className: "text-sm text-commerce-muted",
914
- children: "Connect a store to show products."
1296
+ children: text("connectPrompt")
915
1297
  }) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
916
1298
  role: "status",
917
1299
  className: "text-sm text-commerce-muted",
918
- children: "Connecting to the store…"
1300
+ children: text("connecting")
919
1301
  }) : /* @__PURE__ */ jsxs("div", {
920
1302
  role: "alert",
921
1303
  className: errorNoticeClass,
@@ -923,7 +1305,7 @@ function CommerceStatus() {
923
1305
  type: "button",
924
1306
  className: errorActionClass,
925
1307
  onClick: retryConnection,
926
- children: "Retry connection"
1308
+ children: text("retryConnection")
927
1309
  })]
928
1310
  }) });
929
1311
  }
@@ -937,6 +1319,7 @@ function CommerceStatus() {
937
1319
  function CartButton({ children } = {}) {
938
1320
  const { setOpen, open, connection } = useCommerce();
939
1321
  const { itemCount } = useCart();
1322
+ const text = useMessage();
940
1323
  const [bump, setBump] = useState(0);
941
1324
  const previous = useRef(itemCount);
942
1325
  useEffect(() => {
@@ -952,7 +1335,7 @@ function CartButton({ children } = {}) {
952
1335
  disabled: connection !== "ready" && connection !== "unbound",
953
1336
  "aria-haspopup": "dialog",
954
1337
  "aria-expanded": open,
955
- "aria-label": custom === null ? `Cart, ${itemCount} ${itemCount === 1 ? "item" : "items"}` : void 0,
1338
+ "aria-label": custom === null ? text("cartButtonLabel", { count: itemCount }) : void 0,
956
1339
  "data-commerce-part": "cart-button",
957
1340
  onClick: () => setOpen(true),
958
1341
  children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
@@ -982,8 +1365,11 @@ function BagIcon() {
982
1365
  const isSoldOut = (available) => available !== null && available <= 0;
983
1366
  /** Why `quantity` cannot be added, or `null` when it can. `available` is `null` when stock is not tracked. */
984
1367
  function quantityProblem(quantity, available) {
985
- if (!Number.isInteger(quantity) || quantity < 1) return "Enter a whole number of 1 or more.";
986
- if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
1368
+ if (!Number.isInteger(quantity) || quantity < 1) return { key: "quantityNotWhole" };
1369
+ if (available !== null && available > 0 && quantity > available) return {
1370
+ key: "quantityOverStock",
1371
+ values: { count: available }
1372
+ };
987
1373
  return null;
988
1374
  }
989
1375
  function AddToCartButton(props) {
@@ -998,16 +1384,21 @@ function AddToCartButton(props) {
998
1384
  */
999
1385
  function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
1000
1386
  const { addItem, pending, hydrating, error, open, connection } = useCommerce();
1387
+ const text = useMessage();
1001
1388
  const [adding, setAdding] = useState(false);
1002
1389
  const reasonId = useId();
1003
1390
  const available = getAvailableInventoryQuantity(sku);
1004
1391
  const soldOut = isSoldOut(available);
1005
1392
  let reason = null;
1006
- if (connection === "unbound") reason = "Connect a store to add items to the cart.";
1007
- else if (connection === "error") reason = "The store could not be reached.";
1008
- else if (connection !== "ready") reason = "Connecting to the store…";
1009
- else if (!sku.id) reason = "This item cannot be added to the cart.";
1010
- else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
1393
+ if (connection === "unbound") reason = text("addReasonUnbound");
1394
+ else if (connection === "error") reason = text("addReasonError");
1395
+ else if (connection !== "ready") reason = text("connecting");
1396
+ else if (!sku.id) reason = text("addReasonNoSku");
1397
+ else if (!soldOut && hydrating) reason = text("loadingCart");
1398
+ else if (!soldOut) {
1399
+ const problem = quantityProblem(quantity, available);
1400
+ reason = problem && text(problem.key, problem.values);
1401
+ }
1011
1402
  const disabled = adding || soldOut || reason !== null;
1012
1403
  const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
1013
1404
  busy: false,
@@ -1042,7 +1433,7 @@ function AddToCart({ sku, name, quantity = 1, children, className, block = false
1042
1433
  children: renderLabel(children, {
1043
1434
  busy: adding,
1044
1435
  soldOut
1045
- }, soldOut ? "Out of stock" : adding ? "Adding…" : label ?? "Add to cart")
1436
+ }, soldOut ? text("outOfStock") : adding ? text("adding") : label ?? text("addToCart"))
1046
1437
  }),
1047
1438
  reason && /* @__PURE__ */ jsx("span", {
1048
1439
  id: reasonId,
@@ -1158,11 +1549,12 @@ function CartDrawerRoot({ children, className }) {
1158
1549
  function CartTitle({ className, children }) {
1159
1550
  const { isSample, itemCount } = useCartInternal("CartTitle");
1160
1551
  const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
1552
+ const text = useMessage();
1161
1553
  return /* @__PURE__ */ jsx("div", {
1162
1554
  ...partProps("cart-title", className),
1163
1555
  children: /* @__PURE__ */ jsx(Heading, {
1164
1556
  className: "text-lg font-semibold tracking-tight",
1165
- children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [isSample ? "Sample cart" : "Your cart", itemCount > 0 && /* @__PURE__ */ jsxs("span", {
1557
+ children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [text(isSample ? "sampleCartTitle" : "cartTitle"), itemCount > 0 && /* @__PURE__ */ jsxs("span", {
1166
1558
  className: "font-normal text-commerce-muted",
1167
1559
  children: [
1168
1560
  " (",
@@ -1179,7 +1571,8 @@ function CartDescription({ className, children }) {
1179
1571
  const drawer = useContext(CartDrawerContext);
1180
1572
  const register = drawer?.register;
1181
1573
  useEffect(() => register?.("description"), [register]);
1182
- const text = renderLabel(children, { isSample }, isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.");
1574
+ const message = useMessage();
1575
+ const text = renderLabel(children, { isSample }, isSample ? message("sampleCartDescription", { currency: sampleCurrency }) : message("cartDescription"));
1183
1576
  return /* @__PURE__ */ jsx("div", {
1184
1577
  ...partProps("cart-description", className),
1185
1578
  children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
@@ -1194,12 +1587,13 @@ function CartDescription({ className, children }) {
1194
1587
  /** The drawer's close button. Renders nothing outside the drawer. */
1195
1588
  function CartClose({ className }) {
1196
1589
  useCartInternal("CartClose");
1590
+ const text = useMessage();
1197
1591
  if (!useContext(CartDrawerContext)) return null;
1198
1592
  return /* @__PURE__ */ jsx("div", {
1199
1593
  ...partProps("cart-close", className),
1200
1594
  children: /* @__PURE__ */ jsx(Dialog.Close, {
1201
1595
  className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full text-commerce-muted transition-colors hover:bg-commerce-tint hover:text-inherit focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
1202
- "aria-label": "Close cart",
1596
+ "aria-label": text("closeCart"),
1203
1597
  children: /* @__PURE__ */ jsx("svg", {
1204
1598
  "aria-hidden": "true",
1205
1599
  viewBox: "0 0 20 20",
@@ -1218,6 +1612,7 @@ function CartClose({ className }) {
1218
1612
  /** A storage warning, and a cart error with a refresh button. Renders nothing when there are neither. */
1219
1613
  function CartNotices({ className }) {
1220
1614
  const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
1615
+ const text = useMessage();
1221
1616
  if (!storageWarning && !error) return null;
1222
1617
  return /* @__PURE__ */ jsx("div", {
1223
1618
  ...partProps("cart-notices", className),
@@ -1237,7 +1632,7 @@ function CartNotices({ className }) {
1237
1632
  "aria-disabled": locked,
1238
1633
  "aria-busy": action === "refresh",
1239
1634
  onClick: () => run("refresh", refresh),
1240
- children: "Refresh cart"
1635
+ children: text("refreshCart")
1241
1636
  })]
1242
1637
  })]
1243
1638
  })
@@ -1251,7 +1646,11 @@ function useCartItemInternal(part) {
1251
1646
  if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
1252
1647
  return item;
1253
1648
  }
1254
- /** Read the line from inside `CartItemRoot`, for custom item parts. */
1649
+ /**
1650
+ * Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
1651
+ * `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
1652
+ * samples from more than one catalog, read the site's own fields as possibly missing.
1653
+ */
1255
1654
  function useCartItem() {
1256
1655
  return useCartItemInternal("useCartItem");
1257
1656
  }
@@ -1261,6 +1660,7 @@ function useCartItem() {
1261
1660
  */
1262
1661
  function CartItems({ className, children }) {
1263
1662
  const { items, hydrating } = useCartInternal("CartItems");
1663
+ const text = useMessage();
1264
1664
  const template = hasChildren(children) ? children : packagedItem;
1265
1665
  const registry = usePartRegistry();
1266
1666
  const { has } = registry;
@@ -1273,7 +1673,7 @@ function CartItems({ className, children }) {
1273
1673
  children: [hydrating && /* @__PURE__ */ jsx("p", {
1274
1674
  role: "status",
1275
1675
  className: "py-4 text-sm text-commerce-muted",
1276
- children: "Loading your cart…"
1676
+ children: text("loadingCart")
1277
1677
  }), /* @__PURE__ */ jsx("div", {
1278
1678
  role: "list",
1279
1679
  ...partProps("cart-items", className),
@@ -1306,13 +1706,15 @@ function CartItemRoot({ children, className }) {
1306
1706
  /** The line's product image. Renders nothing without one. */
1307
1707
  function CartItemImage({ className }) {
1308
1708
  const { imageUrl } = useCartItemInternal("CartItemImage");
1309
- if (!imageUrl) return null;
1709
+ const [failedUrl, setFailedUrl] = useState(null);
1710
+ if (!imageUrl || imageUrl === failedUrl) return null;
1310
1711
  return /* @__PURE__ */ jsx("div", {
1311
1712
  ...partProps("cart-item-image", className),
1312
1713
  children: /* @__PURE__ */ jsx("img", {
1313
1714
  src: imageUrl,
1314
1715
  alt: "",
1315
- className: "h-20 w-20 bg-commerce-tint object-cover"
1716
+ className: "h-20 w-20 bg-commerce-tint object-cover",
1717
+ onError: () => setFailedUrl(imageUrl)
1316
1718
  })
1317
1719
  });
1318
1720
  }
@@ -1330,17 +1732,16 @@ function CartItemName({ className }) {
1330
1732
  /** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
1331
1733
  function CartItemOptions({ className }) {
1332
1734
  const { options } = useCartItemInternal("CartItemOptions");
1735
+ const text = useMessage();
1333
1736
  if (!options.length) return null;
1334
1737
  return /* @__PURE__ */ jsx("div", {
1335
1738
  ...partProps("cart-item-options", className),
1336
1739
  children: /* @__PURE__ */ jsx("p", {
1337
1740
  className: "text-sm text-commerce-muted",
1338
- children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [
1339
- index > 0 && " · ",
1340
- option.attribute,
1341
- ": ",
1342
- option.values.join(", ")
1343
- ] }, `${option.attribute}:${option.values.join(",")}`))
1741
+ children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [index > 0 && " · ", text("itemOption", {
1742
+ attribute: option.attribute,
1743
+ values: option.values.join(", ")
1744
+ })] }, `${option.attribute}:${option.values.join(",")}`))
1344
1745
  })
1345
1746
  });
1346
1747
  }
@@ -1348,11 +1749,12 @@ function CartItemOptions({ className }) {
1348
1749
  function CartItemPrice({ className }) {
1349
1750
  const { subtotal, currency } = useCartItemInternal("CartItemPrice");
1350
1751
  const money$1 = useMoney();
1752
+ const text = useMessage();
1351
1753
  return /* @__PURE__ */ jsx("div", {
1352
1754
  ...partProps("cart-item-price", className),
1353
1755
  children: /* @__PURE__ */ jsx("p", {
1354
1756
  className: "text-sm leading-6 tabular-nums",
1355
- children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
1757
+ children: subtotal === null ? text("priceUnavailable") : money$1(subtotal, currency)
1356
1758
  })
1357
1759
  });
1358
1760
  }
@@ -1375,6 +1777,7 @@ function StepperIcon$1({ plus }) {
1375
1777
  function CartItemQuantity({ className }) {
1376
1778
  const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
1377
1779
  const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
1780
+ const text = useMessage();
1378
1781
  return /* @__PURE__ */ jsx("div", {
1379
1782
  ...partProps("cart-item-quantity", className),
1380
1783
  children: /* @__PURE__ */ jsxs("div", {
@@ -1383,7 +1786,7 @@ function CartItemQuantity({ className }) {
1383
1786
  /* @__PURE__ */ jsx("button", {
1384
1787
  type: "button",
1385
1788
  className: stepperButtonClass$1,
1386
- "aria-label": `Decrease quantity of ${name}`,
1789
+ "aria-label": text("decreaseQuantity", { name }),
1387
1790
  "data-commerce-control": "decrease",
1388
1791
  disabled: action === `decrease:${id}` || !id || quantity <= 1,
1389
1792
  "aria-disabled": locked || !id || quantity <= 1,
@@ -1395,13 +1798,13 @@ function CartItemQuantity({ className }) {
1395
1798
  className: "min-w-8 text-center text-sm tabular-nums",
1396
1799
  children: [/* @__PURE__ */ jsx("span", {
1397
1800
  className: "sr-only",
1398
- children: "Quantity: "
1801
+ children: text("quantityPrefix")
1399
1802
  }), quantity]
1400
1803
  }),
1401
1804
  /* @__PURE__ */ jsx("button", {
1402
1805
  type: "button",
1403
1806
  className: stepperButtonClass$1,
1404
- "aria-label": `Increase quantity of ${name}`,
1807
+ "aria-label": text("increaseQuantity", { name }),
1405
1808
  "data-commerce-control": "increase",
1406
1809
  disabled: action === `increase:${id}` || !id,
1407
1810
  "aria-disabled": locked || !id,
@@ -1417,6 +1820,7 @@ function CartItemQuantity({ className }) {
1417
1820
  function CartItemRemove({ className, children }) {
1418
1821
  const { id, name } = useCartItemInternal("CartItemRemove");
1419
1822
  const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
1823
+ const text = useMessage();
1420
1824
  return /* @__PURE__ */ jsx("div", {
1421
1825
  ...partProps("cart-item-remove", className),
1422
1826
  children: /* @__PURE__ */ jsxs("button", {
@@ -1426,7 +1830,7 @@ function CartItemRemove({ className, children }) {
1426
1830
  "aria-disabled": locked || !id,
1427
1831
  "aria-busy": action === `remove:${id}`,
1428
1832
  onClick: () => run(`remove:${id}`, () => removeItem(id)),
1429
- children: [renderLabel(children, { busy: action === `remove:${id}` }, "Remove"), /* @__PURE__ */ jsxs("span", {
1833
+ children: [renderLabel(children, { busy: action === `remove:${id}` }, text("remove")), /* @__PURE__ */ jsxs("span", {
1430
1834
  className: "sr-only",
1431
1835
  children: [" ", name]
1432
1836
  })]
@@ -1452,6 +1856,7 @@ const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
1452
1856
  /** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
1453
1857
  function CartEmpty({ className, children }) {
1454
1858
  const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
1859
+ const text = useMessage();
1455
1860
  if (hydrating || items.length) return null;
1456
1861
  return /* @__PURE__ */ jsx("div", {
1457
1862
  ...partProps("cart-empty", className),
@@ -1474,17 +1879,17 @@ function CartEmpty({ className, children }) {
1474
1879
  }),
1475
1880
  /* @__PURE__ */ jsx("p", {
1476
1881
  className: "text-base font-semibold",
1477
- children: "Your cart is empty"
1882
+ children: text("cartEmpty")
1478
1883
  }),
1479
1884
  /* @__PURE__ */ jsx("p", {
1480
1885
  className: "mt-1 text-sm text-commerce-muted",
1481
- children: isSample ? "Add a sample product to try the cart." : "Add something you love and it will show up here."
1886
+ children: text(isSample ? "sampleCartEmptyHint" : "cartEmptyHint")
1482
1887
  }),
1483
1888
  /* @__PURE__ */ jsx(Link, {
1484
1889
  className: `${secondaryButtonClass} mt-6`,
1485
1890
  to: catalogPath,
1486
1891
  onClick: close,
1487
- children: "Browse products"
1892
+ children: text("browseProducts")
1488
1893
  })
1489
1894
  ] })
1490
1895
  });
@@ -1502,12 +1907,13 @@ function CartSummary({ className, children }) {
1502
1907
  function CartSubtotal({ className }) {
1503
1908
  const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
1504
1909
  const money$1 = useMoney();
1910
+ const text = useMessage();
1505
1911
  if (!items.length || subtotal === null) return null;
1506
1912
  return /* @__PURE__ */ jsx("div", {
1507
1913
  ...partProps("cart-subtotal", className),
1508
1914
  children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
1509
1915
  className: "flex justify-between gap-4 font-semibold",
1510
- children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", {
1916
+ children: [/* @__PURE__ */ jsx("dt", { children: text(isSample ? "sampleSubtotal" : "subtotal") }), /* @__PURE__ */ jsx("dd", {
1511
1917
  className: "tabular-nums",
1512
1918
  children: money$1(subtotal, currency)
1513
1919
  })]
@@ -1520,12 +1926,13 @@ function CartSubtotal({ className }) {
1520
1926
  */
1521
1927
  function CartCheckoutNote({ className }) {
1522
1928
  const { items, isSample } = useCartInternal("CartCheckoutNote");
1929
+ const text = useMessage();
1523
1930
  if (isSample || !items.length) return null;
1524
1931
  return /* @__PURE__ */ jsx("div", {
1525
1932
  ...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
1526
1933
  children: /* @__PURE__ */ jsxs("dl", {
1527
1934
  className: "flex justify-between gap-4 text-sm text-commerce-muted",
1528
- children: [/* @__PURE__ */ jsx("dt", { children: "Shipping, taxes & discounts" }), /* @__PURE__ */ jsx("dd", { children: "Calculated at checkout" })]
1935
+ children: [/* @__PURE__ */ jsx("dt", { children: text("checkoutAdjustments") }), /* @__PURE__ */ jsx("dd", { children: text("checkoutAdjustmentsValue") })]
1529
1936
  })
1530
1937
  });
1531
1938
  }
@@ -1535,15 +1942,16 @@ function CartCheckoutNote({ className }) {
1535
1942
  */
1536
1943
  function CartCheckout({ className, children }) {
1537
1944
  const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
1945
+ const text = useMessage();
1538
1946
  if (!items.length) return null;
1539
1947
  let content;
1540
1948
  if (isSample) content = /* @__PURE__ */ jsx("p", {
1541
1949
  className: "text-sm text-commerce-muted",
1542
- children: "Example total in USD. Checkout is not set up yet."
1950
+ children: text("sampleCheckoutNote", { currency: sampleCurrency })
1543
1951
  });
1544
1952
  else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
1545
1953
  className: "text-sm text-commerce-muted",
1546
- children: "Checkout is not available yet."
1954
+ children: text("checkoutUnavailable")
1547
1955
  });
1548
1956
  else content = /* @__PURE__ */ jsx("button", {
1549
1957
  type: "button",
@@ -1552,7 +1960,7 @@ function CartCheckout({ className, children }) {
1552
1960
  "aria-disabled": locked || !checkoutReady,
1553
1961
  "aria-busy": action === "checkout",
1554
1962
  onClick: () => run("checkout", checkout),
1555
- children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to checkout")
1963
+ children: renderLabel(children, { busy: action === "checkout" }, text(action === "checkout" ? "openingCheckout" : "checkout"))
1556
1964
  });
1557
1965
  return /* @__PURE__ */ jsx("div", {
1558
1966
  ...partProps("cart-checkout", className),
@@ -1566,8 +1974,9 @@ function CartCheckout({ className, children }) {
1566
1974
  function CartContinueShopping({ className, children }) {
1567
1975
  const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
1568
1976
  const inDrawer = !!useContext(CartDrawerContext);
1977
+ const text = useMessage();
1569
1978
  if (!items.length) return null;
1570
- const label = renderLabel(children, { isSample }, "Continue shopping");
1979
+ const label = renderLabel(children, { isSample }, text("continueShopping"));
1571
1980
  const linkClass = "inline-flex min-h-11 w-full items-center justify-center rounded-lg px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current";
1572
1981
  return /* @__PURE__ */ jsx("div", {
1573
1982
  ...partProps("cart-continue-shopping", className),
@@ -1626,6 +2035,7 @@ function CartDrawer() {
1626
2035
  */
1627
2036
  function ProductImage({ url, name, className = "" }) {
1628
2037
  const [failedUrl, setFailedUrl] = useState(null);
2038
+ const text = useMessage();
1629
2039
  return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
1630
2040
  src: url,
1631
2041
  alt: name,
@@ -1635,20 +2045,20 @@ function ProductImage({ url, name, className = "" }) {
1635
2045
  }) : /* @__PURE__ */ jsx("div", {
1636
2046
  className: `flex h-full min-h-44 items-center justify-center bg-commerce-tint p-6 text-center text-sm text-commerce-muted ${className}`,
1637
2047
  role: "img",
1638
- "aria-label": `${name}: image unavailable`,
1639
- children: "No image available"
2048
+ "aria-label": text("imageUnavailable", { name }),
2049
+ children: text("noImage")
1640
2050
  });
1641
2051
  }
1642
2052
  /** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
1643
2053
  const CatalogItemContext = createContext(null);
1644
- const titleLink = "title-link";
1645
- function cardValue(item, productPath, links) {
2054
+ const titleLink = "title";
2055
+ function cardValue(item, productPath, links, sampleNameText) {
1646
2056
  if (item.source === "sample") return {
1647
2057
  ...links,
1648
2058
  source: "sample",
1649
2059
  sample: item.sample,
1650
2060
  product: null,
1651
- name: item.sample.name,
2061
+ name: sampleNameText,
1652
2062
  href: null,
1653
2063
  isSample: true
1654
2064
  };
@@ -1667,16 +2077,24 @@ function useProductCardInternal(part) {
1667
2077
  if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
1668
2078
  return context;
1669
2079
  }
1670
- /** Read the card's product, name, and link from inside `ProductCardRoot`. */
2080
+ /**
2081
+ * Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
2082
+ * samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
2083
+ * on a page that also shows the packaged samples, read the site's own fields as possibly missing.
2084
+ */
1671
2085
  function useProductCard() {
1672
- const { product, name, href, isSample } = useProductCardInternal("useProductCard");
2086
+ const card = useProductCardInternal("useProductCard");
2087
+ const { product, name, href, isSample } = card;
2088
+ const sample = card.source === "sample" ? card.sample : null;
1673
2089
  return useMemo(() => ({
1674
2090
  product,
2091
+ sample,
1675
2092
  name,
1676
2093
  href,
1677
2094
  isSample
1678
2095
  }), [
1679
2096
  product,
2097
+ sample,
1680
2098
  name,
1681
2099
  href,
1682
2100
  isSample
@@ -1686,6 +2104,7 @@ function useProductCard() {
1686
2104
  function ProductCardRoot({ children, product, className }) {
1687
2105
  const catalogItem = useContext(CatalogItemContext);
1688
2106
  const { config } = useCommerce();
2107
+ const text = useMessage();
1689
2108
  const [imageLinked, setImageLinked] = useState(false);
1690
2109
  const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
1691
2110
  useEffect(() => {
@@ -1696,16 +2115,18 @@ function ProductCardRoot({ children, product, className }) {
1696
2115
  source: "live",
1697
2116
  product
1698
2117
  } : null);
2118
+ const sampleText = item?.source === "sample" ? sampleName(text, item.sample) : "";
1699
2119
  return item && cardValue(item, config.productPath, {
1700
2120
  imageLinked,
1701
2121
  register
1702
- });
2122
+ }, sampleText);
1703
2123
  }, [
1704
2124
  catalogItem,
1705
2125
  product,
1706
2126
  config.productPath,
1707
2127
  imageLinked,
1708
- register
2128
+ register,
2129
+ text
1709
2130
  ]);
1710
2131
  if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
1711
2132
  if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
@@ -1722,8 +2143,9 @@ function ProductCardRoot({ children, product, className }) {
1722
2143
  return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
1723
2144
  }
1724
2145
  /**
1725
- * The product's first image, linked to its page. Samples show a placeholder. With a `ProductCardTitle` in the card,
1726
- * the title is the one link keyboard and screen reader users get; without one, the image link is.
2146
+ * The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
2147
+ * placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
2148
+ * and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
1727
2149
  */
1728
2150
  function ProductCardImage({ className }) {
1729
2151
  const card = useProductCardInternal("ProductCardImage");
@@ -1733,23 +2155,30 @@ function ProductCardImage({ className }) {
1733
2155
  to: card.href,
1734
2156
  tabIndex: card.imageLinked ? void 0 : -1,
1735
2157
  "aria-hidden": card.imageLinked ? void 0 : "true",
1736
- className: "block aspect-square overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
2158
+ className: "block aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
1737
2159
  children: /* @__PURE__ */ jsx(ProductImage, {
1738
2160
  url: getPrimaryImageUrl(card.product),
1739
2161
  name: card.name,
1740
2162
  className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
1741
2163
  })
1742
- }) : /* @__PURE__ */ jsx("div", {
2164
+ }) : isBuiltInSample(card.sample) ? /* @__PURE__ */ jsx("div", {
1743
2165
  "aria-hidden": "true",
1744
- className: "flex aspect-square items-center justify-center bg-commerce-tint",
2166
+ className: "flex aspect-(--commerce-card-image-aspect,1/1) items-center justify-center bg-commerce-tint",
1745
2167
  children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
2168
+ }) : /* @__PURE__ */ jsx("div", {
2169
+ "aria-hidden": card.imageLinked ? void 0 : "true",
2170
+ className: "aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint",
2171
+ children: /* @__PURE__ */ jsx(ProductImage, {
2172
+ url: card.sample.imageUrl,
2173
+ name: card.name
2174
+ })
1746
2175
  })
1747
2176
  });
1748
2177
  }
1749
2178
  /** The product name as an H2, linked to its page. */
1750
2179
  function ProductCardTitle({ className }) {
1751
2180
  const { name, href, register } = useProductCardInternal("ProductCardTitle");
1752
- useEffect(() => href ? register(titleLink) : void 0, [href, register]);
2181
+ useEffect(() => register(titleLink), [register]);
1753
2182
  return /* @__PURE__ */ jsx("div", {
1754
2183
  ...partProps("product-card-title", className),
1755
2184
  children: /* @__PURE__ */ jsx("h2", {
@@ -1762,47 +2191,55 @@ function ProductCardTitle({ className }) {
1762
2191
  })
1763
2192
  });
1764
2193
  }
1765
- /** Up to three lines of the product description. Renders nothing without one. */
2194
+ /** Up to two lines of the product or sample description. Renders nothing without one. */
1766
2195
  function ProductCardDescription({ className }) {
1767
- const { product } = useProductCardInternal("ProductCardDescription");
1768
- if (!product?.description) return null;
2196
+ const card = useProductCardInternal("ProductCardDescription");
2197
+ const description = card.source === "sample" ? card.sample.description : card.product.description;
2198
+ if (!description) return null;
1769
2199
  return /* @__PURE__ */ jsx("div", {
1770
2200
  ...partProps("product-card-description", className),
1771
2201
  children: /* @__PURE__ */ jsx("p", {
1772
2202
  className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
1773
- children: product.description
2203
+ children: description
1774
2204
  })
1775
2205
  });
1776
2206
  }
1777
- const formatRange = (range, money$1) => {
2207
+ const formatRange = (range, money$1, moneyRange$1) => {
1778
2208
  const min = range?.min;
1779
2209
  const max = range?.max;
1780
2210
  if (typeof min !== "number") return null;
1781
- return `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}`;
2211
+ return typeof max === "number" && max !== min ? moneyRange$1(min, max) : money$1(min);
1782
2212
  };
1783
2213
  /**
1784
2214
  * "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
1785
- * when the lowest compare-at price is above the highest price. Renders nothing otherwise, or for samples.
2215
+ * when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
2216
+ * price. Renders nothing otherwise.
1786
2217
  */
1787
2218
  function ProductCardBadge({ className }) {
1788
- const { product } = useProductCardInternal("ProductCardBadge");
1789
- if (!isProductOnSale(product)) return null;
2219
+ const card = useProductCardInternal("ProductCardBadge");
2220
+ const text = useMessage();
2221
+ if (card.source === "sample" ? sampleCompareAt(card.sample) === null : !isProductOnSale(card.product)) return null;
1790
2222
  return /* @__PURE__ */ jsx("div", {
1791
2223
  ...partProps("product-card-badge", className),
1792
2224
  children: /* @__PURE__ */ jsx("span", {
1793
2225
  className: saleBadgeClass,
1794
- children: "Sale"
2226
+ children: text("sale")
1795
2227
  })
1796
2228
  });
1797
2229
  }
2230
+ const sampleCompareAt = (sample) => sample.compareAtPrice !== void 0 && sample.compareAtPrice > sample.price ? sample.compareAtPrice : null;
1798
2231
  /** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
1799
2232
  function ProductCardPrice({ className }) {
1800
2233
  const card = useProductCardInternal("ProductCardPrice");
1801
2234
  const money$1 = useMoney();
1802
- const text = card.source === "sample" ? money$1(card.sample.price, "USD") : formatRange(card.product.priceRange, money$1) ?? "Price unavailable";
2235
+ const moneyRange$1 = useMoneyRange();
2236
+ const message = useMessage();
2237
+ const text = card.source === "sample" ? money$1(card.sample.price, sampleCurrency) : formatRange(card.product.priceRange, money$1, moneyRange$1) ?? message("priceUnavailable");
1803
2238
  const range = card.source === "live" ? card.product.priceRange : null;
1804
- const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
1805
- const compareAt = starting === null ? null : money$1(starting);
2239
+ let starting = null;
2240
+ if (card.source === "sample") starting = sampleCompareAt(card.sample);
2241
+ else if (range?.min === range?.max) starting = getStartingCompareAt(card.product);
2242
+ const compareAt = starting === null ? null : money$1(starting, card.source === "sample" ? sampleCurrency : void 0);
1806
2243
  return /* @__PURE__ */ jsx("div", {
1807
2244
  ...partProps("product-card-price", className),
1808
2245
  children: /* @__PURE__ */ jsxs("p", {
@@ -1812,7 +2249,7 @@ function ProductCardPrice({ className }) {
1812
2249
  children: text
1813
2250
  }), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1814
2251
  className: "sr-only",
1815
- children: "Was"
2252
+ children: message("wasPrice")
1816
2253
  }), /* @__PURE__ */ jsx("del", {
1817
2254
  className: "text-commerce-muted",
1818
2255
  children: compareAt
@@ -1827,6 +2264,7 @@ function ProductCardPrice({ className }) {
1827
2264
  function ProductCardSwatches({ className }) {
1828
2265
  const { product } = useProductCardInternal("ProductCardSwatches");
1829
2266
  const { getSwatch } = useCommerce();
2267
+ const text = useMessage();
1830
2268
  if (!getSwatch || !product) return null;
1831
2269
  for (const attribute of getProductAttributes(product)) {
1832
2270
  const swatches = attribute.values.map((value) => ({
@@ -1844,16 +2282,13 @@ function ProductCardSwatches({ className }) {
1844
2282
  children: /* @__PURE__ */ jsxs("p", {
1845
2283
  className: "flex items-center gap-1.5 text-xs text-commerce-muted",
1846
2284
  children: [
1847
- /* @__PURE__ */ jsxs("span", {
2285
+ /* @__PURE__ */ jsx("span", {
1848
2286
  className: "sr-only",
1849
- children: [
1850
- swatches.length,
1851
- " ",
1852
- inSentence(attribute.label),
1853
- " options:",
1854
- " ",
1855
- swatches.map((swatch) => swatch.label).join(", ")
1856
- ]
2287
+ children: text("swatchSummary", {
2288
+ count: swatches.length,
2289
+ attribute: inSentence(attribute.label),
2290
+ values: swatches.map((swatch) => swatch.label).join(", ")
2291
+ })
1857
2292
  }),
1858
2293
  shown.map((swatch) => /* @__PURE__ */ jsx("span", {
1859
2294
  "aria-hidden": "true",
@@ -1892,6 +2327,7 @@ function ProductCardAction({ className, children }) {
1892
2327
  function LiveAction({ product, name, href, label }) {
1893
2328
  const selectedSku = getSingleMatchedSku(product);
1894
2329
  const hasVariants = getProductAttributes(product).length > 0;
2330
+ const text = useMessage();
1895
2331
  if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
1896
2332
  sku: selectedSku,
1897
2333
  name,
@@ -1910,11 +2346,13 @@ function LiveAction({ product, name, href, label }) {
1910
2346
  action,
1911
2347
  busy: false,
1912
2348
  soldOut: false
1913
- }, hasVariants ? "Choose options" : "View product")
2349
+ }, text(hasVariants ? "chooseOptions" : "viewProduct"))
1914
2350
  });
1915
2351
  }
1916
2352
  function SampleAction({ sample, label }) {
1917
2353
  const { add } = useSampleCart();
2354
+ const text = useMessage();
2355
+ const name = sampleName(text, sample);
1918
2356
  const custom = renderLabel(label, {
1919
2357
  action: "add",
1920
2358
  busy: false,
@@ -1923,20 +2361,16 @@ function SampleAction({ sample, label }) {
1923
2361
  return custom === null ? /* @__PURE__ */ jsx("button", {
1924
2362
  type: "button",
1925
2363
  className: `${secondaryButtonClass} w-full`,
1926
- "aria-label": `Add ${sample.name} to sample cart`,
1927
- onClick: () => add(sample.key),
1928
- children: "Add to cart"
2364
+ "aria-label": text("addSampleToCart", { name }),
2365
+ onClick: () => add(sample),
2366
+ children: text("addToCart")
1929
2367
  }) : /* @__PURE__ */ jsxs("button", {
1930
2368
  type: "button",
1931
2369
  className: `${secondaryButtonClass} w-full`,
1932
- onClick: () => add(sample.key),
2370
+ onClick: () => add(sample),
1933
2371
  children: [custom, /* @__PURE__ */ jsxs("span", {
1934
2372
  className: "sr-only",
1935
- children: [
1936
- " (",
1937
- sample.name,
1938
- ", sample)"
1939
- ]
2373
+ children: [" ", text("sampleItemName", { name })]
1940
2374
  })]
1941
2375
  });
1942
2376
  }
@@ -1984,14 +2418,39 @@ function useRootParams(urlState) {
1984
2418
  //#region src/catalog.tsx
1985
2419
  const CatalogContext = createContext(null);
1986
2420
  const CatalogEmptyContext = createContext(null);
1987
- const sampleCatalog = { source: "sample" };
2421
+ const builtInCatalog = {
2422
+ source: "sample",
2423
+ samples: builtInSamples
2424
+ };
1988
2425
  function useCatalogInternal(part) {
1989
2426
  const context = useContext(CatalogContext);
1990
2427
  if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
1991
2428
  return context;
1992
2429
  }
1993
- /** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
1994
- function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false, pageSize = 6, urlState = true }) {
2430
+ /** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
2431
+ function useCatalog() {
2432
+ const catalog = useCatalogInternal("useCatalog");
2433
+ let count;
2434
+ let status = "ready";
2435
+ if (catalog.source === "sample") count = catalog.samples.length;
2436
+ else {
2437
+ count = catalog.nodes.length;
2438
+ if (catalog.products.isPending) status = "loading";
2439
+ else if (catalog.products.isError) status = "error";
2440
+ }
2441
+ const isSample = catalog.source === "sample";
2442
+ return useMemo(() => ({
2443
+ count,
2444
+ isSample,
2445
+ status
2446
+ }), [
2447
+ count,
2448
+ isSample,
2449
+ status
2450
+ ]);
2451
+ }
2452
+ /** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
2453
+ function CatalogRoot({ children, className, sampleProducts = false, pageSize = 6, urlState = true }) {
1995
2454
  const { connection } = useCommerce();
1996
2455
  const [custom, setCustom] = useState(false);
1997
2456
  const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
@@ -2003,6 +2462,25 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
2003
2462
  custom
2004
2463
  }), [register, custom]);
2005
2464
  if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
2465
+ const hostSamples = useStableSamples(Array.isArray(sampleProducts) ? sampleProducts : null);
2466
+ const problem = useMemo(() => hostSamples && sampleProblem(hostSamples), [hostSamples]);
2467
+ if (problem && connection === "unbound") throw new Error(problem);
2468
+ useEffect(() => {
2469
+ if (problem && process.env.NODE_ENV !== "production") console.error(`[gd-commerce-storefront] ${problem}`);
2470
+ }, [problem]);
2471
+ const { refresh } = useSampleCart();
2472
+ useEffect(() => {
2473
+ if (hostSamples && !problem && connection === "unbound") refresh(hostSamples);
2474
+ }, [
2475
+ hostSamples,
2476
+ problem,
2477
+ connection,
2478
+ refresh
2479
+ ]);
2480
+ const sampleCatalog = useMemo(() => hostSamples ? {
2481
+ source: "sample",
2482
+ samples: hostSamples
2483
+ } : builtInCatalog, [hostSamples]);
2006
2484
  let content;
2007
2485
  if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
2008
2486
  className,
@@ -2010,7 +2488,7 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
2010
2488
  urlState,
2011
2489
  children
2012
2490
  });
2013
- else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
2491
+ else if (connection === "unbound" && sampleProducts) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
2014
2492
  value: sampleCatalog,
2015
2493
  children: /* @__PURE__ */ jsx("section", {
2016
2494
  ...partProps("catalog-root", className),
@@ -2027,6 +2505,13 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
2027
2505
  children: content
2028
2506
  }) });
2029
2507
  }
2508
+ /** The previous list while the samples and their fields are unchanged, so an inline list does not rebuild the cards. */
2509
+ function useStableSamples(samples) {
2510
+ const [stable, setStable] = useState(samples);
2511
+ const same = stable === samples || !!stable && !!samples && sameSamples(stable, samples);
2512
+ if (!same) setStable(samples);
2513
+ return same ? stable : samples;
2514
+ }
2030
2515
  function CatalogLoader({ children, className, pageSize, urlState }) {
2031
2516
  const { config } = useCommerce();
2032
2517
  const [params, setParams] = useRootParams(urlState);
@@ -2075,46 +2560,47 @@ function CatalogLoader({ children, className, pageSize, urlState }) {
2075
2560
  });
2076
2561
  }
2077
2562
  /** The catalog title as the page's H1, with a description. */
2078
- function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
2563
+ function CatalogHeader({ title, description, className }) {
2079
2564
  useCatalogInternal("CatalogHeader");
2565
+ const text = useMessage();
2080
2566
  return /* @__PURE__ */ jsxs("header", {
2081
2567
  ...partProps("catalog-header", className),
2082
- children: [/* @__PURE__ */ jsx("h1", {
2568
+ children: [title !== null && /* @__PURE__ */ jsx("h1", {
2083
2569
  className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
2084
- children: title
2085
- }), /* @__PURE__ */ jsx("p", {
2570
+ children: title === void 0 ? text("catalogTitle") : title
2571
+ }), description !== null && /* @__PURE__ */ jsx("p", {
2086
2572
  className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
2087
- children: description
2573
+ children: description === void 0 ? text("catalogDescription") : description
2088
2574
  })]
2089
2575
  });
2090
2576
  }
2091
2577
  /** Announces loading, failure, or how many products the page shows. */
2092
2578
  function CatalogSummary({ className }) {
2093
- const catalog = useCatalogInternal("CatalogSummary");
2579
+ const { key, values } = summary(useCatalogInternal("CatalogSummary"));
2580
+ const text = useMessage();
2094
2581
  return /* @__PURE__ */ jsx("div", {
2095
2582
  ...partProps("catalog-summary", className),
2096
2583
  children: /* @__PURE__ */ jsx("p", {
2097
2584
  className: "text-sm text-commerce-muted",
2098
2585
  role: "status",
2099
- children: summary(catalog)
2586
+ children: text(key, values)
2100
2587
  })
2101
2588
  });
2102
2589
  }
2103
2590
  function summary(catalog) {
2104
- if (catalog.source === "sample") return "Sample products · Example prices in USD";
2105
- if (catalog.products.isPending) return "Loading products…";
2106
- if (catalog.products.isError) return "Products unavailable";
2107
- return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
2591
+ if (catalog.source === "sample") return {
2592
+ key: "sampleCatalogSummary",
2593
+ values: { currency: sampleCurrency }
2594
+ };
2595
+ if (catalog.products.isPending) return { key: "loadingProducts" };
2596
+ if (catalog.products.isError) return { key: "productsUnavailable" };
2597
+ return {
2598
+ key: "productCount",
2599
+ values: { count: catalog.nodes.length }
2600
+ };
2108
2601
  }
2109
2602
  /** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
2110
2603
  const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
2111
- const sampleItems = sampleProducts.map((sample) => ({
2112
- key: sample.key,
2113
- item: {
2114
- source: "sample",
2115
- sample
2116
- }
2117
- }));
2118
2604
  const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
2119
2605
  className: packagedCardClass,
2120
2606
  children: /* @__PURE__ */ jsx(PackagedCardParts, {})
@@ -2128,28 +2614,37 @@ function CatalogProducts({ className, children }) {
2128
2614
  const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
2129
2615
  const card = hasChildren(children) ? children : packagedCard$1;
2130
2616
  const nodes = catalog.source === "live" ? catalog.nodes : null;
2617
+ const samples = catalog.source === "sample" ? catalog.samples : null;
2131
2618
  const items = useMemo(() => nodes ? nodes.map((product) => ({
2132
2619
  key: product.id ?? "",
2133
2620
  item: {
2134
2621
  source: "live",
2135
2622
  product
2136
2623
  }
2137
- })) : sampleItems, [nodes]);
2624
+ })) : (samples ?? []).map((sample) => ({
2625
+ key: sample.key,
2626
+ item: {
2627
+ source: "sample",
2628
+ sample
2629
+ }
2630
+ })), [nodes, samples]);
2138
2631
  const products = catalog.source === "live" ? catalog.products : null;
2632
+ const { messages } = useCommerce();
2633
+ const text = useMessage();
2139
2634
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2140
2635
  products?.isError && /* @__PURE__ */ jsxs("div", {
2141
2636
  role: "alert",
2142
2637
  className: errorNoticeClass,
2143
- children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
2638
+ children: [/* @__PURE__ */ jsx("p", { children: describeError(products.error, messages) }), /* @__PURE__ */ jsx("button", {
2144
2639
  type: "button",
2145
2640
  className: errorActionClass,
2146
2641
  onClick: () => void products.refetch(),
2147
- children: "Retry products"
2642
+ children: text("retryProducts")
2148
2643
  })]
2149
2644
  }),
2150
2645
  products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
2151
2646
  className: "py-16 text-center text-commerce-muted",
2152
- children: "No products available."
2647
+ children: text("noProducts")
2153
2648
  }),
2154
2649
  /* @__PURE__ */ jsx("div", {
2155
2650
  ...partProps("catalog-products", className),
@@ -2161,7 +2656,7 @@ function CatalogProducts({ className, children }) {
2161
2656
  "aria-hidden": "true",
2162
2657
  className: "motion-safe:animate-pulse",
2163
2658
  children: [
2164
- /* @__PURE__ */ jsx("div", { className: "aspect-square bg-commerce-tint-strong" }),
2659
+ /* @__PURE__ */ jsx("div", { className: "aspect-(--commerce-card-image-aspect,1/1) bg-commerce-tint-strong" }),
2165
2660
  /* @__PURE__ */ jsxs("div", {
2166
2661
  className: "space-y-2 pt-3",
2167
2662
  children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
@@ -2183,10 +2678,11 @@ function CatalogEmpty({ className, children }) {
2183
2678
  const catalog = useCatalogInternal("CatalogEmpty");
2184
2679
  const register = useContext(CatalogEmptyContext)?.register;
2185
2680
  useEffect(() => register?.("empty"), [register]);
2681
+ const text = useMessage();
2186
2682
  if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
2187
2683
  return /* @__PURE__ */ jsx("div", {
2188
2684
  ...partProps("catalog-empty", className),
2189
- children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
2685
+ children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: text("noProducts") })
2190
2686
  });
2191
2687
  }
2192
2688
  /**
@@ -2194,13 +2690,14 @@ function CatalogEmpty({ className, children }) {
2194
2690
  */
2195
2691
  function CatalogPagination({ className }) {
2196
2692
  const catalog = useCatalogInternal("CatalogPagination");
2693
+ const text = useMessage();
2197
2694
  if (catalog.source === "sample") return null;
2198
2695
  const { after, goToPage } = catalog;
2199
2696
  const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
2200
2697
  const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
2201
2698
  if (!after && !hasNext) return null;
2202
2699
  return /* @__PURE__ */ jsx("nav", {
2203
- "aria-label": "Product pages",
2700
+ "aria-label": text("productPages"),
2204
2701
  ...partProps("catalog-pagination", className),
2205
2702
  children: /* @__PURE__ */ jsxs("div", {
2206
2703
  className: "flex justify-center gap-3",
@@ -2208,20 +2705,20 @@ function CatalogPagination({ className }) {
2208
2705
  type: "button",
2209
2706
  className: secondaryButtonClass,
2210
2707
  onClick: () => goToPage(),
2211
- children: "First page"
2708
+ children: text("firstPage")
2212
2709
  }), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
2213
2710
  type: "button",
2214
2711
  className: secondaryButtonClass,
2215
2712
  onClick: () => goToPage(pageInfo.endCursor ?? void 0),
2216
- children: "Next page"
2713
+ children: text("nextPage")
2217
2714
  })]
2218
2715
  })
2219
2716
  });
2220
2717
  }
2221
2718
  /** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
2222
- function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1, pageSize }) {
2719
+ function Catalog({ title, description, showHeader = true, sampleProducts, pageSize }) {
2223
2720
  return /* @__PURE__ */ jsxs(CatalogRoot, {
2224
- sampleProducts: sampleProducts$1,
2721
+ sampleProducts,
2225
2722
  pageSize,
2226
2723
  children: [
2227
2724
  /* @__PURE__ */ jsxs("div", {
@@ -2325,7 +2822,8 @@ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
2325
2822
  }) });
2326
2823
  }
2327
2824
  function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
2328
- const { config, getSwatch } = useCommerce();
2825
+ const { config, getSwatch, messages } = useCommerce();
2826
+ const text = useMessage();
2329
2827
  const [params, setParams] = useRootParams(urlState);
2330
2828
  const [quantity, setQuantity] = useState(1);
2331
2829
  const [imageIndex, setImageIndex] = useState(0);
@@ -2403,7 +2901,7 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2403
2901
  children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
2404
2902
  role: "status",
2405
2903
  className: "text-sm text-commerce-muted",
2406
- children: "Loading product…"
2904
+ children: text("loadingProduct")
2407
2905
  })
2408
2906
  });
2409
2907
  if (product.isError && !productNotFound) {
@@ -2412,22 +2910,22 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2412
2910
  role: "alert",
2413
2911
  ...partProps("product-details-root", className),
2414
2912
  children: renderLabel(error, {
2415
- message: message(product.error),
2913
+ message: describeError(product.error, messages),
2416
2914
  retry
2417
2915
  }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
2418
2916
  /* @__PURE__ */ jsx("h1", {
2419
2917
  className: "text-2xl font-semibold tracking-tight",
2420
- children: "Product unavailable"
2918
+ children: text("productUnavailable")
2421
2919
  }),
2422
2920
  /* @__PURE__ */ jsx("p", {
2423
2921
  className: `my-4 ${errorNoticeClass}`,
2424
- children: message(product.error)
2922
+ children: describeError(product.error, messages)
2425
2923
  }),
2426
2924
  /* @__PURE__ */ jsx("button", {
2427
2925
  type: "button",
2428
2926
  className: buttonClass,
2429
2927
  onClick: retry,
2430
- children: "Retry product"
2928
+ children: text("retryProduct")
2431
2929
  })
2432
2930
  ] }))
2433
2931
  });
@@ -2436,11 +2934,11 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2436
2934
  ...partProps("product-details-root", className),
2437
2935
  children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
2438
2936
  className: "text-2xl font-semibold tracking-tight",
2439
- children: "Product not found"
2937
+ children: text("productNotFound")
2440
2938
  }), /* @__PURE__ */ jsx(Link, {
2441
2939
  className: `${buttonClass} mt-6`,
2442
2940
  to: config.catalogPath,
2443
- children: "Back to shop"
2941
+ children: text("backToShop")
2444
2942
  })] })
2445
2943
  });
2446
2944
  const value = {
@@ -2539,11 +3037,11 @@ function getSoldOutValues(group, attributes, selections) {
2539
3037
  //#endregion
2540
3038
  //#region src/product-purchase.tsx
2541
3039
  const disabledReasons = {
2542
- incomplete: "Select all options to add this item to your cart.",
2543
- checking: "Checking this variant…",
2544
- "variant-error": "Availability for this selection could not be checked.",
2545
- unavailable: "This combination is unavailable. Choose different options.",
2546
- "not-purchasable": "This product is not currently available to purchase."
3040
+ incomplete: "selectAllOptions",
3041
+ checking: "checkingVariant",
3042
+ "variant-error": "variantCheckFailed",
3043
+ unavailable: "combinationUnavailable",
3044
+ "not-purchasable": "notPurchasable"
2547
3045
  };
2548
3046
  /** Above this many values, buttons wrap into a wall, so the option is a select. */
2549
3047
  const maxChoiceButtons = 8;
@@ -2608,14 +3106,11 @@ const swatchClass = "relative block h-9 w-9 overflow-hidden rounded-full border
2608
3106
  function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2609
3107
  const name = useId();
2610
3108
  const errorId = useId();
2611
- const error = missing && /* @__PURE__ */ jsxs("p", {
3109
+ const text = useMessage();
3110
+ const error = missing && /* @__PURE__ */ jsx("p", {
2612
3111
  id: errorId,
2613
3112
  className: "mt-2 inline-block rounded bg-red-50 px-2 py-1 text-sm text-red-800",
2614
- children: [
2615
- "Select ",
2616
- inSentence(label),
2617
- "."
2618
- ]
3113
+ children: text("selectAttributeError", { attribute: inSentence(label) })
2619
3114
  });
2620
3115
  if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
2621
3116
  /* @__PURE__ */ jsx("legend", {
@@ -2628,12 +3123,12 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2628
3123
  "aria-describedby": missing ? errorId : void 0,
2629
3124
  value: selected,
2630
3125
  onChange: (event) => onSelect(event.target.value),
2631
- children: [/* @__PURE__ */ jsxs("option", {
3126
+ children: [/* @__PURE__ */ jsx("option", {
2632
3127
  value: "",
2633
- children: ["Select ", inSentence(label)]
3128
+ children: text("selectAttribute", { attribute: inSentence(label) })
2634
3129
  }), choices.map((choice) => /* @__PURE__ */ jsx("option", {
2635
3130
  value: choice.value,
2636
- children: choice.soldOut ? `${choice.label} (sold out)` : choice.label
3131
+ children: choice.soldOut ? text("soldOutOption", { label: choice.label }) : choice.label
2637
3132
  }, choice.value))]
2638
3133
  }),
2639
3134
  error
@@ -2654,7 +3149,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2654
3149
  className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
2655
3150
  children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
2656
3151
  className: "group relative cursor-pointer",
2657
- title: swatches ? `${choice.label}${choice.soldOut ? ", sold out" : ""}` : void 0,
3152
+ title: swatches ? choice.soldOut ? text("soldOutChoice", { label: choice.label }) : choice.label : void 0,
2658
3153
  children: [/* @__PURE__ */ jsx("input", {
2659
3154
  type: "radio",
2660
3155
  className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
@@ -2662,7 +3157,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2662
3157
  value: choice.value,
2663
3158
  checked: choice.value === selected,
2664
3159
  "aria-invalid": missing || void 0,
2665
- "aria-label": choice.soldOut ? `${choice.label}, sold out` : void 0,
3160
+ "aria-label": choice.soldOut ? text("soldOutChoice", { label: choice.label }) : void 0,
2666
3161
  "data-commerce-control": "choice",
2667
3162
  onChange: () => onSelect(choice.value)
2668
3163
  }), swatches ? /* @__PURE__ */ jsxs("span", {
@@ -2688,12 +3183,13 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2688
3183
  function SkuOption() {
2689
3184
  const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
2690
3185
  useSkuOptionRegistration();
3186
+ const text = useMessage();
2691
3187
  const missing = showMissing && !explicitSkuId;
2692
3188
  return /* @__PURE__ */ jsx("div", {
2693
3189
  ...partProps("product-details-sku-option"),
2694
3190
  "data-commerce-missing": missing || void 0,
2695
3191
  children: /* @__PURE__ */ jsx(ChoiceGroup, {
2696
- label: "Option",
3192
+ label: text("skuOptionLabel"),
2697
3193
  selected: explicitSkuId,
2698
3194
  missing,
2699
3195
  onSelect: selectSku,
@@ -2737,6 +3233,8 @@ function ProductDetailsOption({ attribute, className }) {
2737
3233
  */
2738
3234
  function ProductDetailsPurchaseStatus({ className }) {
2739
3235
  const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
3236
+ const { messages } = useCommerce();
3237
+ const text = useMessage();
2740
3238
  let content;
2741
3239
  switch (status) {
2742
3240
  case "ready":
@@ -2745,11 +3243,11 @@ function ProductDetailsPurchaseStatus({ className }) {
2745
3243
  content = /* @__PURE__ */ jsxs("div", {
2746
3244
  role: "alert",
2747
3245
  className: errorNoticeClass,
2748
- children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
3246
+ children: [/* @__PURE__ */ jsx("p", { children: describeError(variantError, messages) }), /* @__PURE__ */ jsx("button", {
2749
3247
  type: "button",
2750
3248
  className: errorActionClass,
2751
3249
  onClick: retryVariant,
2752
- children: "Retry availability"
3250
+ children: text("retryAvailability")
2753
3251
  })]
2754
3252
  });
2755
3253
  break;
@@ -2757,20 +3255,20 @@ function ProductDetailsPurchaseStatus({ className }) {
2757
3255
  content = /* @__PURE__ */ jsx("p", {
2758
3256
  role: "status",
2759
3257
  className: errorNoticeClass,
2760
- children: disabledReasons[status]
3258
+ children: text(disabledReasons[status])
2761
3259
  });
2762
3260
  break;
2763
3261
  case "not-purchasable":
2764
3262
  content = /* @__PURE__ */ jsx("p", {
2765
3263
  className: "text-sm text-commerce-muted",
2766
- children: disabledReasons[status]
3264
+ children: text(disabledReasons[status])
2767
3265
  });
2768
3266
  break;
2769
3267
  case "checking":
2770
3268
  content = /* @__PURE__ */ jsx("p", {
2771
3269
  role: "status",
2772
3270
  className: "text-sm text-commerce-muted",
2773
- children: disabledReasons[status]
3271
+ children: text(disabledReasons[status])
2774
3272
  });
2775
3273
  break;
2776
3274
  }
@@ -2782,13 +3280,14 @@ function ProductDetailsPurchaseStatus({ className }) {
2782
3280
  /** Stock for the verified SKU. Renders nothing until a SKU is verified. */
2783
3281
  function ProductDetailsAvailability({ className }) {
2784
3282
  const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
3283
+ const text = useMessage();
2785
3284
  if (!selectedSku) return null;
2786
3285
  return /* @__PURE__ */ jsx("div", {
2787
3286
  ...partProps("product-details-availability", className),
2788
3287
  children: /* @__PURE__ */ jsx("p", {
2789
3288
  className: "text-sm text-commerce-muted",
2790
3289
  "aria-live": "polite",
2791
- children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
3290
+ children: availableQuantity === null ? text("availableToOrder") : isSoldOut(availableQuantity) ? text("variantOutOfStock") : text("availableCount", { count: availableQuantity })
2792
3291
  })
2793
3292
  });
2794
3293
  }
@@ -2815,9 +3314,11 @@ function ProductDetailsQuantity({ className }) {
2815
3314
  const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
2816
3315
  const id = useId();
2817
3316
  const problemId = useId();
3317
+ const text = useMessage();
2818
3318
  const soldOut = !!selectedSku && isSoldOut(availableQuantity);
2819
3319
  const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
2820
- const problem = soldOut ? null : quantityProblem(quantity, max);
3320
+ const issue = soldOut ? null : quantityProblem(quantity, max);
3321
+ const problem = issue && text(issue.key, issue.values);
2821
3322
  const current = Number.isInteger(quantity) ? quantity : 1;
2822
3323
  return /* @__PURE__ */ jsxs("div", {
2823
3324
  ...partProps("product-details-quantity", className),
@@ -2825,7 +3326,7 @@ function ProductDetailsQuantity({ className }) {
2825
3326
  /* @__PURE__ */ jsx("label", {
2826
3327
  htmlFor: id,
2827
3328
  className: "sr-only",
2828
- children: "Quantity"
3329
+ children: text("quantity")
2829
3330
  }),
2830
3331
  /* @__PURE__ */ jsxs("div", {
2831
3332
  className: "inline-flex items-center rounded-lg border border-commerce-edge",
@@ -2833,7 +3334,7 @@ function ProductDetailsQuantity({ className }) {
2833
3334
  /* @__PURE__ */ jsx("button", {
2834
3335
  type: "button",
2835
3336
  className: stepperButtonClass,
2836
- "aria-label": `Decrease quantity of ${name}`,
3337
+ "aria-label": text("decreaseQuantity", { name }),
2837
3338
  "data-commerce-control": "decrease",
2838
3339
  disabled: soldOut || current <= 1,
2839
3340
  onClick: () => setQuantity(current - 1),
@@ -2855,7 +3356,7 @@ function ProductDetailsQuantity({ className }) {
2855
3356
  /* @__PURE__ */ jsx("button", {
2856
3357
  type: "button",
2857
3358
  className: stepperButtonClass,
2858
- "aria-label": `Increase quantity of ${name}`,
3359
+ "aria-label": text("increaseQuantity", { name }),
2859
3360
  "data-commerce-control": "increase",
2860
3361
  disabled: soldOut || max !== null && current >= max,
2861
3362
  onClick: () => setQuantity(current + 1),
@@ -2880,6 +3381,7 @@ function ProductDetailsAddToCart({ className, children }) {
2880
3381
  const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
2881
3382
  const { config } = useCommerce();
2882
3383
  const money$1 = useMoney();
3384
+ const text = useMessage();
2883
3385
  const reasonId = useId();
2884
3386
  if (selectedSku) {
2885
3387
  const unit = price?.value?.value;
@@ -2892,8 +3394,8 @@ function ProductDetailsAddToCart({ className, children }) {
2892
3394
  quantity,
2893
3395
  className: buttonClass,
2894
3396
  block: true,
2895
- label: total && `Add to cart · ${total}`,
2896
- labelName: total ? `Add to cart, ${total}` : void 0,
3397
+ label: total && text("addToCartWithPrice", { price: total }),
3398
+ labelName: total ? text("addToCartWithPriceName", { price: total }) : void 0,
2897
3399
  children
2898
3400
  })
2899
3401
  });
@@ -2910,7 +3412,7 @@ function ProductDetailsAddToCart({ className, children }) {
2910
3412
  children: renderLabel(children, {
2911
3413
  busy: false,
2912
3414
  soldOut: false
2913
- }, next ? `Select ${inSentence(next)}` : "Select an option")
3415
+ }, next ? text("selectAttribute", { attribute: inSentence(next) }) : text("selectOption"))
2914
3416
  })
2915
3417
  });
2916
3418
  }
@@ -2925,11 +3427,11 @@ function ProductDetailsAddToCart({ className, children }) {
2925
3427
  children: renderLabel(children, {
2926
3428
  busy: false,
2927
3429
  soldOut: false
2928
- }, "Add to cart")
3430
+ }, text("addToCart"))
2929
3431
  }), /* @__PURE__ */ jsx("span", {
2930
3432
  id: reasonId,
2931
3433
  hidden: true,
2932
- children: status === "ready" ? "" : disabledReasons[status]
3434
+ children: status === "ready" ? "" : text(disabledReasons[status])
2933
3435
  })]
2934
3436
  });
2935
3437
  }
@@ -2958,6 +3460,7 @@ function ProductDetails() {
2958
3460
  function ProductDetailsBackLink({ className, children }) {
2959
3461
  useProductDetailsInternal("ProductDetailsBackLink");
2960
3462
  const { config } = useCommerce();
3463
+ const text = useMessage();
2961
3464
  return /* @__PURE__ */ jsx("div", {
2962
3465
  ...partProps("product-details-back-link", className),
2963
3466
  children: /* @__PURE__ */ jsx(Link, {
@@ -2975,13 +3478,14 @@ function ProductDetailsBackLink({ className, children }) {
2975
3478
  strokeLinecap: "round",
2976
3479
  strokeLinejoin: "round"
2977
3480
  })
2978
- }), "Back to shop"] }))
3481
+ }), text("backToShop")] }))
2979
3482
  })
2980
3483
  });
2981
3484
  }
2982
3485
  /** Main image with thumbnails; the selected SKU's images replace the product's when present. */
2983
3486
  function ProductDetailsGallery({ className }) {
2984
3487
  const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
3488
+ const text = useMessage();
2985
3489
  return /* @__PURE__ */ jsxs("div", {
2986
3490
  ...partProps("product-details-gallery", className),
2987
3491
  children: [/* @__PURE__ */ jsx("div", {
@@ -2995,7 +3499,7 @@ function ProductDetailsGallery({ className }) {
2995
3499
  children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
2996
3500
  type: "button",
2997
3501
  className: `h-20 w-20 shrink-0 overflow-hidden bg-commerce-tint outline-offset-2 transition-opacity focus-visible:outline focus-visible:outline-2 focus-visible:outline-current ${imageIndex === index ? "outline outline-2 outline-current" : "opacity-60 hover:opacity-100"}`,
2998
- "aria-label": `View image ${index + 1}`,
3502
+ "aria-label": text("viewImage", { number: index + 1 }),
2999
3503
  "aria-pressed": imageIndex === index,
3000
3504
  "data-commerce-control": "thumbnail",
3001
3505
  onClick: () => setImageIndex(index),
@@ -3013,12 +3517,13 @@ function ProductDetailsGallery({ className }) {
3013
3517
  */
3014
3518
  function ProductDetailsBadge({ className }) {
3015
3519
  const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
3520
+ const text = useMessage();
3016
3521
  if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
3017
3522
  return /* @__PURE__ */ jsx("div", {
3018
3523
  ...partProps("product-details-badge", className),
3019
3524
  children: /* @__PURE__ */ jsx("span", {
3020
3525
  className: saleBadgeClass,
3021
- children: "Sale"
3526
+ children: text("sale")
3022
3527
  })
3023
3528
  });
3024
3529
  }
@@ -3041,6 +3546,7 @@ function ProductDetailsPrice({ className }) {
3041
3546
  const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
3042
3547
  const { config } = useCommerce();
3043
3548
  const money$1 = useMoney();
3549
+ const text = useMessage();
3044
3550
  let compareAt = null;
3045
3551
  if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
3046
3552
  else if (!price) {
@@ -3053,9 +3559,9 @@ function ProductDetailsPrice({ className }) {
3053
3559
  "aria-live": "polite",
3054
3560
  className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
3055
3561
  "data-testid": "product-price",
3056
- children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money$1(product.priceRange.min)}` : "Price unavailable", compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3562
+ children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? text("priceFrom", { price: money$1(product.priceRange.min) }) : text("priceUnavailable"), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3057
3563
  className: "sr-only",
3058
- children: "Was"
3564
+ children: text("wasPrice")
3059
3565
  }), /* @__PURE__ */ jsx("del", {
3060
3566
  className: "text-base font-normal text-commerce-muted",
3061
3567
  children: compareAt
@@ -3087,10 +3593,11 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
3087
3593
  * the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
3088
3594
  * fails, or while no store is connected.
3089
3595
  */
3090
- function RelatedProducts({ productId: productIdProp, title = "You may also like", limit = 4, className, children }) {
3596
+ function RelatedProducts({ productId: productIdProp, title, limit = 4, className, children }) {
3091
3597
  const { productId: routeProductId = "" } = useParams();
3092
3598
  const productId = productIdProp ?? routeProductId;
3093
3599
  const { config, connection } = useCommerce();
3600
+ const text = useMessage();
3094
3601
  const headingId = useId();
3095
3602
  const first = limit + 1;
3096
3603
  const products = useQuery({
@@ -3117,12 +3624,12 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
3117
3624
  if (connection !== "ready" || !related.length) return null;
3118
3625
  const card = hasChildren(children) ? children : packagedCard;
3119
3626
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
3120
- "aria-labelledby": headingId,
3627
+ "aria-labelledby": title === null ? void 0 : headingId,
3121
3628
  ...partProps("related-products", className),
3122
- children: [/* @__PURE__ */ jsx("h2", {
3629
+ children: [title !== null && /* @__PURE__ */ jsx("h2", {
3123
3630
  id: headingId,
3124
3631
  className: "mb-6 text-xl font-semibold tracking-tight",
3125
- children: title
3632
+ children: title === void 0 ? text("relatedTitle") : title
3126
3633
  }), /* @__PURE__ */ jsx("div", {
3127
3634
  className: catalogGridClass,
3128
3635
  children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
@@ -3137,4 +3644,4 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
3137
3644
  }
3138
3645
 
3139
3646
  //#endregion
3140
- export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };
3647
+ export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };