@godaddy/gd-commerce-storefront 0.9.0 → 0.10.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,
@@ -103,9 +350,13 @@ function getCartSummaryTotals(order) {
103
350
  //#region src/sample-cart.tsx
104
351
  const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
105
352
  key: `sample-${index + 1}`,
106
- name: `Product ${index + 1}`,
353
+ number: index + 1,
107
354
  price: (index + 1) * 1e3
108
355
  }));
356
+ /** Sample prices are examples, in this currency whatever the store's. */
357
+ const sampleCurrency = "USD";
358
+ /** A sample's name, such as "Product 1". The card and the cart both name samples with this. */
359
+ const sampleName = (text, sample) => text("sampleProductName", { number: sample.number });
109
360
  const noQuantities = {};
110
361
  const SampleCartContext = createContext(null);
111
362
  function useSampleCart() {
@@ -113,7 +364,8 @@ function useSampleCart() {
113
364
  if (!value) throw new Error("Mount CommerceProvider around sample products.");
114
365
  return value;
115
366
  }
116
- function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
367
+ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children }) {
368
+ const text = useCallback((key, values) => formatMessage(messages, key, values), [messages]);
117
369
  const session = useMemo(() => ({ enabled }), [enabled]);
118
370
  const active = useRef(session);
119
371
  active.current = session;
@@ -134,8 +386,9 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
134
386
  const quantities = state.session === session && enabled ? state.quantities : noQuantities;
135
387
  const items = useMemo(() => sampleProducts.flatMap((product) => quantities[product.key] ? [{
136
388
  ...product,
389
+ name: sampleName(text, product),
137
390
  quantity: quantities[product.key]
138
- }] : []), [quantities]);
391
+ }] : []), [quantities, text]);
139
392
  function update(key, quantity) {
140
393
  if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
141
394
  if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
@@ -159,13 +412,13 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
159
412
  add: (key) => {
160
413
  const product = sampleProducts.find((product$1) => product$1.key === key);
161
414
  if (!product || !update(key)) return false;
162
- onAdd(product.name);
415
+ onAdd(sampleName(text, product));
163
416
  return true;
164
417
  },
165
418
  changeQuantity: async (key, quantity) => {
166
419
  const updated = quantity >= 1 && update(key, quantity);
167
420
  if (updated) {
168
- announce("Sample cart quantity updated.");
421
+ announce("sampleQuantityUpdated");
169
422
  emit({
170
423
  type: "quantity-changed",
171
424
  itemId: key,
@@ -178,7 +431,7 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
178
431
  remove: async (key) => {
179
432
  const updated = update(key, 0);
180
433
  if (updated) {
181
- announce("Sample item removed from cart.");
434
+ announce("sampleItemRemoved");
182
435
  emit({
183
436
  type: "item-removed",
184
437
  itemId: key,
@@ -211,12 +464,50 @@ function useMoney() {
211
464
  const { currencyCode } = config;
212
465
  return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
213
466
  }
467
+ /** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
468
+ function useMoneyRange() {
469
+ const { locale, config } = useCommerce();
470
+ const { currencyCode } = config;
471
+ return useCallback((min, max, currency = currencyCode) => moneyRange(min, max, currency, locale), [locale, currencyCode]);
472
+ }
473
+ /**
474
+ * The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
475
+ * it. For custom parts that should match the package's language. English outside a provider.
476
+ */
477
+ function useMessage() {
478
+ const messages = useContext(CommerceContext)?.messages ?? enUS;
479
+ return useCallback((key, values) => formatMessage(messages, key, values), [messages]);
480
+ }
214
481
  const CONNECTING_FAILURE_GRACE_MS = 6e4;
215
482
  /** Polls every five seconds for five minutes after the page loads, then every minute, and stops at thirty. */
216
483
  const FAST_POLL_FOR_MS = 5 * 6e4;
217
484
  const STOP_POLLING_AT_MS = 30 * 6e4;
218
485
  /** A failed retry keeps showing connecting this long so the click visibly does something. */
219
486
  const RETRY_FEEDBACK_MS = 1e3;
487
+ /**
488
+ * `messages` while its contents are unchanged, so an inline object, including inline `errorCodes` or plural forms,
489
+ * does not change every render. The kept value is state, so a render React discards cannot leave it behind.
490
+ */
491
+ function useStableMessages(value) {
492
+ const [stable, setStable] = useState(value);
493
+ if (stable === value || sameMessages(stable, value)) return stable;
494
+ setStable(() => value);
495
+ return value;
496
+ }
497
+ /** Equal keys and values, comparing plain-object values, such as `errorCodes` and plural forms, one level deeper. */
498
+ function sameMessages(a, b, deeper = true) {
499
+ if (a === b) return true;
500
+ if (!a || !b) return false;
501
+ const left = a;
502
+ const right = b;
503
+ const keys = Object.keys(left);
504
+ 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)));
505
+ }
506
+ function isPlainObject(value) {
507
+ if (typeof value !== "object" || value === null) return false;
508
+ const prototype = Object.getPrototypeOf(value);
509
+ return prototype === Object.prototype || prototype === null;
510
+ }
220
511
  /** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
221
512
  function resolveLocale(locale) {
222
513
  if (!locale) return "en-US";
@@ -243,18 +534,28 @@ function callHost(callback) {
243
534
  async function loadConfig(signal) {
244
535
  const config = await request("/config", { signal });
245
536
  if (config?.state === "unbound" || config?.state === "connecting") {
246
- if ("cartScope" in config || "currencyCode" in config) throw new Error("The store returned invalid configuration.");
537
+ if ("cartScope" in config || "currencyCode" in config) throw new StorefrontError("invalidConfiguration");
247
538
  return { state: config.state };
248
539
  }
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.");
540
+ 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
541
  return {
251
542
  state: "ready",
252
543
  cartScope: config.cartScope,
253
544
  currencyCode: config.currencyCode
254
545
  };
255
546
  }
256
- function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
547
+ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, messages, onItemAdded, onEvent }) {
257
548
  const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
549
+ const stableMessages = useStableMessages(messages);
550
+ const resolvedMessages = useMemo(() => ({
551
+ ...enUS,
552
+ ...stableMessages,
553
+ locale: stableMessages?.locale || enUS.locale,
554
+ errorCodes: {
555
+ ...enUS.errorCodes,
556
+ ...stableMessages?.errorCodes
557
+ }
558
+ }), [stableMessages]);
258
559
  const [pollingSince, setPollingSince] = useState(Date.now);
259
560
  const [pollingStopped, setPollingStopped] = useState(false);
260
561
  const [retrying, setRetrying] = useState(false);
@@ -300,7 +601,7 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
300
601
  checkoutSuccessPath
301
602
  },
302
603
  connection,
303
- connectionError: connection !== "error" ? null : stalled ? "The store is taking too long to connect." : message(config.error),
604
+ connectionError: connection !== "error" ? null : stalled ? formatMessage(resolvedMessages, "connectionSlow") : describeError(config.error, resolvedMessages),
304
605
  retryConnection: () => {
305
606
  const started = Date.now();
306
607
  setPollingSince(started);
@@ -317,12 +618,14 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
317
618
  theme,
318
619
  getSwatch,
319
620
  locale: resolvedLocale,
621
+ messages: resolvedMessages,
320
622
  onItemAdded,
321
623
  onEvent,
322
624
  children
323
625
  });
324
626
  }
325
- function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
627
+ function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, messages, onItemAdded, onEvent }) {
628
+ const text = (key, values) => formatMessage(messages, key, values);
326
629
  const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
327
630
  const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
328
631
  const ready = connection === "ready";
@@ -338,8 +641,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
338
641
  const [open, setOpen] = useState(false);
339
642
  const [pending, setPending] = useState(false);
340
643
  const [hydrating, setHydrating] = useState(true);
341
- const [error, setError] = useState(null);
342
- const [storageWarning, setStorageWarning] = useState(null);
644
+ const [failure, setError] = useState(null);
645
+ const [storageUnavailable, setStorageUnavailable] = useState(false);
646
+ const error = failure && describeError(failure.cause, messages);
647
+ const storageWarning = storageUnavailable ? text("storageUnavailable") : null;
343
648
  const [announcement, setAnnouncement] = useState("");
344
649
  const id = useRef(null);
345
650
  const currentCart = useRef(null);
@@ -399,7 +704,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
399
704
  try {
400
705
  return cartIdStorage.read();
401
706
  } catch {
402
- setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
707
+ setStorageUnavailable(true);
403
708
  return id.current;
404
709
  }
405
710
  }
@@ -410,9 +715,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
410
715
  setCart(next);
411
716
  try {
412
717
  cartIdStorage.write(id.current);
413
- setStorageWarning(null);
718
+ setStorageUnavailable(false);
414
719
  } catch {
415
- setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
720
+ setStorageUnavailable(true);
416
721
  }
417
722
  }
418
723
  async function hydrate() {
@@ -446,12 +751,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
446
751
  return isActive();
447
752
  } catch (cause) {
448
753
  if (isActive()) {
449
- const text = message(cause);
450
- setError(text);
754
+ const shown = describeError(cause, messages);
755
+ setError({ cause });
756
+ const code = cause instanceof ApiError ? cause.code : void 0;
451
757
  emit({
452
758
  type: "error",
453
759
  operation: kind,
454
- message: text
760
+ message: shown,
761
+ ...code ? { code } : {}
455
762
  });
456
763
  }
457
764
  return false;
@@ -476,7 +783,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
476
783
  setCart(null);
477
784
  setOpen(false);
478
785
  setError(null);
479
- setStorageWarning(null);
786
+ setStorageUnavailable(false);
480
787
  setAnnouncement("");
481
788
  setPending(false);
482
789
  setHydrating(ready);
@@ -498,7 +805,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
498
805
  function addItem(item) {
499
806
  rememberOpener();
500
807
  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.");
808
+ if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new StorefrontError("invalidAddItem");
502
809
  await hydrate();
503
810
  if (!isActive()) return;
504
811
  const fetchCart = (input, init) => {
@@ -514,10 +821,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
514
821
  name: item.name,
515
822
  quantity: item.quantity
516
823
  }, fetchCart);
517
- if (!result.cart?.id || !result.cart.lineItems?.length) throw new Error("Commerce did not return a cart with items. Refresh before retrying.");
824
+ if (!result.cart?.id || !result.cart.lineItems?.length) throw new StorefrontError("addReturnedNoCart");
518
825
  if (!isActive()) return;
519
826
  commit(result.cart);
520
- setAnnouncement(`${item.name} added to your cart.`);
827
+ setAnnouncement(text("itemAdded", { name: item.name }));
521
828
  openDrawer();
522
829
  emit({
523
830
  type: "item-added",
@@ -532,16 +839,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
532
839
  return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
533
840
  await hydrate();
534
841
  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.");
842
+ if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new StorefrontError("itemNotInCart");
843
+ if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new StorefrontError("invalidQuantity");
537
844
  const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/items/${encodeURIComponent(itemId)}`, {
538
845
  method,
539
846
  ...quantity !== void 0 ? { body: JSON.stringify({ quantity }) } : {}
540
847
  });
541
- if (!result.cart?.id) throw new Error("Commerce did not return the updated cart. Refresh before retrying.");
848
+ if (!result.cart?.id) throw new StorefrontError("updateReturnedNoCart");
542
849
  if (!isActive()) return;
543
850
  commit(result.cart);
544
- setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
851
+ setAnnouncement(text(method === "DELETE" ? "itemRemoved" : "quantityUpdated"));
545
852
  emit(method === "DELETE" ? {
546
853
  type: "item-removed",
547
854
  itemId,
@@ -558,16 +865,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
558
865
  return run("discount", async () => {
559
866
  await hydrate();
560
867
  if (!isActive()) return;
561
- if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
868
+ if (!id.current || !code.trim()) throw new StorefrontError("discountMissing");
562
869
  const codes = [...new Set([...currentCart.current?.discounts?.flatMap((discount) => discount.code ? [discount.code] : []) ?? [], code.trim()])];
563
870
  const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/discounts`, {
564
871
  method: "POST",
565
872
  body: JSON.stringify({ discountCodes: codes })
566
873
  });
567
- if (!result.cart?.id) throw new Error("Commerce did not return the updated cart.");
874
+ if (!result.cart?.id) throw new StorefrontError("discountReturnedNoCart");
568
875
  if (!isActive()) return;
569
876
  commit(result.cart);
570
- setAnnouncement("Promotion codes updated.");
877
+ setAnnouncement(text("discountsUpdated"));
571
878
  emit({
572
879
  type: "discount-applied",
573
880
  code: code.trim()
@@ -576,10 +883,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
576
883
  }
577
884
  function checkout() {
578
885
  return run("checkout", async () => {
579
- if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
886
+ if (!config.checkoutSuccessPath) throw new StorefrontError("checkoutDisabled");
580
887
  await hydrate();
581
888
  if (!isActive()) return;
582
- if (!id.current || !currentCart.current?.lineItems?.length) throw new Error("Add an item before checking out.");
889
+ if (!id.current || !currentCart.current?.lineItems?.length) throw new StorefrontError("checkoutEmpty");
583
890
  const origin = window.location.origin;
584
891
  const successUrl = new URL(config.checkoutSuccessPath, origin);
585
892
  successUrl.searchParams.set("orderId", id.current);
@@ -592,9 +899,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
592
899
  })
593
900
  });
594
901
  if (!isActive()) return;
595
- if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
902
+ if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new StorefrontError("checkoutNoUrl");
596
903
  const url = new URL(session$1.url, origin);
597
- if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
904
+ if (url.protocol !== "https:") throw new StorefrontError("checkoutInvalidUrl");
598
905
  emit({
599
906
  type: "checkout-started",
600
907
  cartId: id.current
@@ -611,6 +918,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
611
918
  theme,
612
919
  getSwatch,
613
920
  locale,
921
+ messages,
614
922
  cart,
615
923
  open,
616
924
  setOpen: (next) => {
@@ -637,10 +945,11 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
637
945
  value: registerDrawer,
638
946
  children: /* @__PURE__ */ jsx(SampleCartProvider, {
639
947
  enabled: connection === "unbound",
640
- announce: setAnnouncement,
948
+ messages,
949
+ announce: (key) => setAnnouncement(text(key)),
641
950
  emit,
642
951
  onAdd: (name) => {
643
- setAnnouncement(`${name} added to your sample cart.`);
952
+ setAnnouncement(text("sampleItemAdded", { name }));
644
953
  openDrawer();
645
954
  emit({
646
955
  type: "item-added",
@@ -678,11 +987,11 @@ function useCart() {
678
987
  imageUrl: null,
679
988
  options: [],
680
989
  subtotal: item.price * item.quantity,
681
- currency: "USD"
990
+ currency: sampleCurrency
682
991
  })),
683
992
  itemCount: sampleItems$1.reduce((total, item) => total + item.quantity, 0),
684
993
  subtotal: sampleItems$1.reduce((sum, item) => sum + item.price * item.quantity, 0),
685
- currency: "USD",
994
+ currency: sampleCurrency,
686
995
  isSample,
687
996
  loading: false
688
997
  };
@@ -907,15 +1216,16 @@ function StorefrontSurface({ children, className = "" }) {
907
1216
  }
908
1217
  function CommerceStatus() {
909
1218
  const { connection, connectionError, retryConnection } = useCommerce();
1219
+ const text = useMessage();
910
1220
  if (connection === "ready") return null;
911
1221
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
912
1222
  role: "status",
913
1223
  className: "text-sm text-commerce-muted",
914
- children: "Connect a store to show products."
1224
+ children: text("connectPrompt")
915
1225
  }) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
916
1226
  role: "status",
917
1227
  className: "text-sm text-commerce-muted",
918
- children: "Connecting to the store…"
1228
+ children: text("connecting")
919
1229
  }) : /* @__PURE__ */ jsxs("div", {
920
1230
  role: "alert",
921
1231
  className: errorNoticeClass,
@@ -923,7 +1233,7 @@ function CommerceStatus() {
923
1233
  type: "button",
924
1234
  className: errorActionClass,
925
1235
  onClick: retryConnection,
926
- children: "Retry connection"
1236
+ children: text("retryConnection")
927
1237
  })]
928
1238
  }) });
929
1239
  }
@@ -937,6 +1247,7 @@ function CommerceStatus() {
937
1247
  function CartButton({ children } = {}) {
938
1248
  const { setOpen, open, connection } = useCommerce();
939
1249
  const { itemCount } = useCart();
1250
+ const text = useMessage();
940
1251
  const [bump, setBump] = useState(0);
941
1252
  const previous = useRef(itemCount);
942
1253
  useEffect(() => {
@@ -952,7 +1263,7 @@ function CartButton({ children } = {}) {
952
1263
  disabled: connection !== "ready" && connection !== "unbound",
953
1264
  "aria-haspopup": "dialog",
954
1265
  "aria-expanded": open,
955
- "aria-label": custom === null ? `Cart, ${itemCount} ${itemCount === 1 ? "item" : "items"}` : void 0,
1266
+ "aria-label": custom === null ? text("cartButtonLabel", { count: itemCount }) : void 0,
956
1267
  "data-commerce-part": "cart-button",
957
1268
  onClick: () => setOpen(true),
958
1269
  children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
@@ -982,8 +1293,11 @@ function BagIcon() {
982
1293
  const isSoldOut = (available) => available !== null && available <= 0;
983
1294
  /** Why `quantity` cannot be added, or `null` when it can. `available` is `null` when stock is not tracked. */
984
1295
  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.`;
1296
+ if (!Number.isInteger(quantity) || quantity < 1) return { key: "quantityNotWhole" };
1297
+ if (available !== null && available > 0 && quantity > available) return {
1298
+ key: "quantityOverStock",
1299
+ values: { count: available }
1300
+ };
987
1301
  return null;
988
1302
  }
989
1303
  function AddToCartButton(props) {
@@ -998,16 +1312,21 @@ function AddToCartButton(props) {
998
1312
  */
999
1313
  function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
1000
1314
  const { addItem, pending, hydrating, error, open, connection } = useCommerce();
1315
+ const text = useMessage();
1001
1316
  const [adding, setAdding] = useState(false);
1002
1317
  const reasonId = useId();
1003
1318
  const available = getAvailableInventoryQuantity(sku);
1004
1319
  const soldOut = isSoldOut(available);
1005
1320
  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);
1321
+ if (connection === "unbound") reason = text("addReasonUnbound");
1322
+ else if (connection === "error") reason = text("addReasonError");
1323
+ else if (connection !== "ready") reason = text("connecting");
1324
+ else if (!sku.id) reason = text("addReasonNoSku");
1325
+ else if (!soldOut && hydrating) reason = text("loadingCart");
1326
+ else if (!soldOut) {
1327
+ const problem = quantityProblem(quantity, available);
1328
+ reason = problem && text(problem.key, problem.values);
1329
+ }
1011
1330
  const disabled = adding || soldOut || reason !== null;
1012
1331
  const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
1013
1332
  busy: false,
@@ -1042,7 +1361,7 @@ function AddToCart({ sku, name, quantity = 1, children, className, block = false
1042
1361
  children: renderLabel(children, {
1043
1362
  busy: adding,
1044
1363
  soldOut
1045
- }, soldOut ? "Out of stock" : adding ? "Adding…" : label ?? "Add to cart")
1364
+ }, soldOut ? text("outOfStock") : adding ? text("adding") : label ?? text("addToCart"))
1046
1365
  }),
1047
1366
  reason && /* @__PURE__ */ jsx("span", {
1048
1367
  id: reasonId,
@@ -1158,11 +1477,12 @@ function CartDrawerRoot({ children, className }) {
1158
1477
  function CartTitle({ className, children }) {
1159
1478
  const { isSample, itemCount } = useCartInternal("CartTitle");
1160
1479
  const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
1480
+ const text = useMessage();
1161
1481
  return /* @__PURE__ */ jsx("div", {
1162
1482
  ...partProps("cart-title", className),
1163
1483
  children: /* @__PURE__ */ jsx(Heading, {
1164
1484
  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", {
1485
+ children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [text(isSample ? "sampleCartTitle" : "cartTitle"), itemCount > 0 && /* @__PURE__ */ jsxs("span", {
1166
1486
  className: "font-normal text-commerce-muted",
1167
1487
  children: [
1168
1488
  " (",
@@ -1179,7 +1499,8 @@ function CartDescription({ className, children }) {
1179
1499
  const drawer = useContext(CartDrawerContext);
1180
1500
  const register = drawer?.register;
1181
1501
  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.");
1502
+ const message = useMessage();
1503
+ const text = renderLabel(children, { isSample }, isSample ? message("sampleCartDescription", { currency: sampleCurrency }) : message("cartDescription"));
1183
1504
  return /* @__PURE__ */ jsx("div", {
1184
1505
  ...partProps("cart-description", className),
1185
1506
  children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
@@ -1194,12 +1515,13 @@ function CartDescription({ className, children }) {
1194
1515
  /** The drawer's close button. Renders nothing outside the drawer. */
1195
1516
  function CartClose({ className }) {
1196
1517
  useCartInternal("CartClose");
1518
+ const text = useMessage();
1197
1519
  if (!useContext(CartDrawerContext)) return null;
1198
1520
  return /* @__PURE__ */ jsx("div", {
1199
1521
  ...partProps("cart-close", className),
1200
1522
  children: /* @__PURE__ */ jsx(Dialog.Close, {
1201
1523
  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",
1524
+ "aria-label": text("closeCart"),
1203
1525
  children: /* @__PURE__ */ jsx("svg", {
1204
1526
  "aria-hidden": "true",
1205
1527
  viewBox: "0 0 20 20",
@@ -1218,6 +1540,7 @@ function CartClose({ className }) {
1218
1540
  /** A storage warning, and a cart error with a refresh button. Renders nothing when there are neither. */
1219
1541
  function CartNotices({ className }) {
1220
1542
  const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
1543
+ const text = useMessage();
1221
1544
  if (!storageWarning && !error) return null;
1222
1545
  return /* @__PURE__ */ jsx("div", {
1223
1546
  ...partProps("cart-notices", className),
@@ -1237,7 +1560,7 @@ function CartNotices({ className }) {
1237
1560
  "aria-disabled": locked,
1238
1561
  "aria-busy": action === "refresh",
1239
1562
  onClick: () => run("refresh", refresh),
1240
- children: "Refresh cart"
1563
+ children: text("refreshCart")
1241
1564
  })]
1242
1565
  })]
1243
1566
  })
@@ -1261,6 +1584,7 @@ function useCartItem() {
1261
1584
  */
1262
1585
  function CartItems({ className, children }) {
1263
1586
  const { items, hydrating } = useCartInternal("CartItems");
1587
+ const text = useMessage();
1264
1588
  const template = hasChildren(children) ? children : packagedItem;
1265
1589
  const registry = usePartRegistry();
1266
1590
  const { has } = registry;
@@ -1273,7 +1597,7 @@ function CartItems({ className, children }) {
1273
1597
  children: [hydrating && /* @__PURE__ */ jsx("p", {
1274
1598
  role: "status",
1275
1599
  className: "py-4 text-sm text-commerce-muted",
1276
- children: "Loading your cart…"
1600
+ children: text("loadingCart")
1277
1601
  }), /* @__PURE__ */ jsx("div", {
1278
1602
  role: "list",
1279
1603
  ...partProps("cart-items", className),
@@ -1330,17 +1654,16 @@ function CartItemName({ className }) {
1330
1654
  /** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
1331
1655
  function CartItemOptions({ className }) {
1332
1656
  const { options } = useCartItemInternal("CartItemOptions");
1657
+ const text = useMessage();
1333
1658
  if (!options.length) return null;
1334
1659
  return /* @__PURE__ */ jsx("div", {
1335
1660
  ...partProps("cart-item-options", className),
1336
1661
  children: /* @__PURE__ */ jsx("p", {
1337
1662
  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(",")}`))
1663
+ children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [index > 0 && " · ", text("itemOption", {
1664
+ attribute: option.attribute,
1665
+ values: option.values.join(", ")
1666
+ })] }, `${option.attribute}:${option.values.join(",")}`))
1344
1667
  })
1345
1668
  });
1346
1669
  }
@@ -1348,11 +1671,12 @@ function CartItemOptions({ className }) {
1348
1671
  function CartItemPrice({ className }) {
1349
1672
  const { subtotal, currency } = useCartItemInternal("CartItemPrice");
1350
1673
  const money$1 = useMoney();
1674
+ const text = useMessage();
1351
1675
  return /* @__PURE__ */ jsx("div", {
1352
1676
  ...partProps("cart-item-price", className),
1353
1677
  children: /* @__PURE__ */ jsx("p", {
1354
1678
  className: "text-sm leading-6 tabular-nums",
1355
- children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
1679
+ children: subtotal === null ? text("priceUnavailable") : money$1(subtotal, currency)
1356
1680
  })
1357
1681
  });
1358
1682
  }
@@ -1375,6 +1699,7 @@ function StepperIcon$1({ plus }) {
1375
1699
  function CartItemQuantity({ className }) {
1376
1700
  const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
1377
1701
  const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
1702
+ const text = useMessage();
1378
1703
  return /* @__PURE__ */ jsx("div", {
1379
1704
  ...partProps("cart-item-quantity", className),
1380
1705
  children: /* @__PURE__ */ jsxs("div", {
@@ -1383,7 +1708,7 @@ function CartItemQuantity({ className }) {
1383
1708
  /* @__PURE__ */ jsx("button", {
1384
1709
  type: "button",
1385
1710
  className: stepperButtonClass$1,
1386
- "aria-label": `Decrease quantity of ${name}`,
1711
+ "aria-label": text("decreaseQuantity", { name }),
1387
1712
  "data-commerce-control": "decrease",
1388
1713
  disabled: action === `decrease:${id}` || !id || quantity <= 1,
1389
1714
  "aria-disabled": locked || !id || quantity <= 1,
@@ -1395,13 +1720,13 @@ function CartItemQuantity({ className }) {
1395
1720
  className: "min-w-8 text-center text-sm tabular-nums",
1396
1721
  children: [/* @__PURE__ */ jsx("span", {
1397
1722
  className: "sr-only",
1398
- children: "Quantity: "
1723
+ children: text("quantityPrefix")
1399
1724
  }), quantity]
1400
1725
  }),
1401
1726
  /* @__PURE__ */ jsx("button", {
1402
1727
  type: "button",
1403
1728
  className: stepperButtonClass$1,
1404
- "aria-label": `Increase quantity of ${name}`,
1729
+ "aria-label": text("increaseQuantity", { name }),
1405
1730
  "data-commerce-control": "increase",
1406
1731
  disabled: action === `increase:${id}` || !id,
1407
1732
  "aria-disabled": locked || !id,
@@ -1417,6 +1742,7 @@ function CartItemQuantity({ className }) {
1417
1742
  function CartItemRemove({ className, children }) {
1418
1743
  const { id, name } = useCartItemInternal("CartItemRemove");
1419
1744
  const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
1745
+ const text = useMessage();
1420
1746
  return /* @__PURE__ */ jsx("div", {
1421
1747
  ...partProps("cart-item-remove", className),
1422
1748
  children: /* @__PURE__ */ jsxs("button", {
@@ -1426,7 +1752,7 @@ function CartItemRemove({ className, children }) {
1426
1752
  "aria-disabled": locked || !id,
1427
1753
  "aria-busy": action === `remove:${id}`,
1428
1754
  onClick: () => run(`remove:${id}`, () => removeItem(id)),
1429
- children: [renderLabel(children, { busy: action === `remove:${id}` }, "Remove"), /* @__PURE__ */ jsxs("span", {
1755
+ children: [renderLabel(children, { busy: action === `remove:${id}` }, text("remove")), /* @__PURE__ */ jsxs("span", {
1430
1756
  className: "sr-only",
1431
1757
  children: [" ", name]
1432
1758
  })]
@@ -1452,6 +1778,7 @@ const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
1452
1778
  /** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
1453
1779
  function CartEmpty({ className, children }) {
1454
1780
  const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
1781
+ const text = useMessage();
1455
1782
  if (hydrating || items.length) return null;
1456
1783
  return /* @__PURE__ */ jsx("div", {
1457
1784
  ...partProps("cart-empty", className),
@@ -1474,17 +1801,17 @@ function CartEmpty({ className, children }) {
1474
1801
  }),
1475
1802
  /* @__PURE__ */ jsx("p", {
1476
1803
  className: "text-base font-semibold",
1477
- children: "Your cart is empty"
1804
+ children: text("cartEmpty")
1478
1805
  }),
1479
1806
  /* @__PURE__ */ jsx("p", {
1480
1807
  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."
1808
+ children: text(isSample ? "sampleCartEmptyHint" : "cartEmptyHint")
1482
1809
  }),
1483
1810
  /* @__PURE__ */ jsx(Link, {
1484
1811
  className: `${secondaryButtonClass} mt-6`,
1485
1812
  to: catalogPath,
1486
1813
  onClick: close,
1487
- children: "Browse products"
1814
+ children: text("browseProducts")
1488
1815
  })
1489
1816
  ] })
1490
1817
  });
@@ -1502,12 +1829,13 @@ function CartSummary({ className, children }) {
1502
1829
  function CartSubtotal({ className }) {
1503
1830
  const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
1504
1831
  const money$1 = useMoney();
1832
+ const text = useMessage();
1505
1833
  if (!items.length || subtotal === null) return null;
1506
1834
  return /* @__PURE__ */ jsx("div", {
1507
1835
  ...partProps("cart-subtotal", className),
1508
1836
  children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
1509
1837
  className: "flex justify-between gap-4 font-semibold",
1510
- children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", {
1838
+ children: [/* @__PURE__ */ jsx("dt", { children: text(isSample ? "sampleSubtotal" : "subtotal") }), /* @__PURE__ */ jsx("dd", {
1511
1839
  className: "tabular-nums",
1512
1840
  children: money$1(subtotal, currency)
1513
1841
  })]
@@ -1520,12 +1848,13 @@ function CartSubtotal({ className }) {
1520
1848
  */
1521
1849
  function CartCheckoutNote({ className }) {
1522
1850
  const { items, isSample } = useCartInternal("CartCheckoutNote");
1851
+ const text = useMessage();
1523
1852
  if (isSample || !items.length) return null;
1524
1853
  return /* @__PURE__ */ jsx("div", {
1525
1854
  ...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
1526
1855
  children: /* @__PURE__ */ jsxs("dl", {
1527
1856
  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" })]
1857
+ children: [/* @__PURE__ */ jsx("dt", { children: text("checkoutAdjustments") }), /* @__PURE__ */ jsx("dd", { children: text("checkoutAdjustmentsValue") })]
1529
1858
  })
1530
1859
  });
1531
1860
  }
@@ -1535,15 +1864,16 @@ function CartCheckoutNote({ className }) {
1535
1864
  */
1536
1865
  function CartCheckout({ className, children }) {
1537
1866
  const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
1867
+ const text = useMessage();
1538
1868
  if (!items.length) return null;
1539
1869
  let content;
1540
1870
  if (isSample) content = /* @__PURE__ */ jsx("p", {
1541
1871
  className: "text-sm text-commerce-muted",
1542
- children: "Example total in USD. Checkout is not set up yet."
1872
+ children: text("sampleCheckoutNote", { currency: sampleCurrency })
1543
1873
  });
1544
1874
  else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
1545
1875
  className: "text-sm text-commerce-muted",
1546
- children: "Checkout is not available yet."
1876
+ children: text("checkoutUnavailable")
1547
1877
  });
1548
1878
  else content = /* @__PURE__ */ jsx("button", {
1549
1879
  type: "button",
@@ -1552,7 +1882,7 @@ function CartCheckout({ className, children }) {
1552
1882
  "aria-disabled": locked || !checkoutReady,
1553
1883
  "aria-busy": action === "checkout",
1554
1884
  onClick: () => run("checkout", checkout),
1555
- children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to checkout")
1885
+ children: renderLabel(children, { busy: action === "checkout" }, text(action === "checkout" ? "openingCheckout" : "checkout"))
1556
1886
  });
1557
1887
  return /* @__PURE__ */ jsx("div", {
1558
1888
  ...partProps("cart-checkout", className),
@@ -1566,8 +1896,9 @@ function CartCheckout({ className, children }) {
1566
1896
  function CartContinueShopping({ className, children }) {
1567
1897
  const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
1568
1898
  const inDrawer = !!useContext(CartDrawerContext);
1899
+ const text = useMessage();
1569
1900
  if (!items.length) return null;
1570
- const label = renderLabel(children, { isSample }, "Continue shopping");
1901
+ const label = renderLabel(children, { isSample }, text("continueShopping"));
1571
1902
  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
1903
  return /* @__PURE__ */ jsx("div", {
1573
1904
  ...partProps("cart-continue-shopping", className),
@@ -1626,6 +1957,7 @@ function CartDrawer() {
1626
1957
  */
1627
1958
  function ProductImage({ url, name, className = "" }) {
1628
1959
  const [failedUrl, setFailedUrl] = useState(null);
1960
+ const text = useMessage();
1629
1961
  return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
1630
1962
  src: url,
1631
1963
  alt: name,
@@ -1635,20 +1967,20 @@ function ProductImage({ url, name, className = "" }) {
1635
1967
  }) : /* @__PURE__ */ jsx("div", {
1636
1968
  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
1969
  role: "img",
1638
- "aria-label": `${name}: image unavailable`,
1639
- children: "No image available"
1970
+ "aria-label": text("imageUnavailable", { name }),
1971
+ children: text("noImage")
1640
1972
  });
1641
1973
  }
1642
1974
  /** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
1643
1975
  const CatalogItemContext = createContext(null);
1644
1976
  const titleLink = "title-link";
1645
- function cardValue(item, productPath, links) {
1977
+ function cardValue(item, productPath, links, sampleNameText) {
1646
1978
  if (item.source === "sample") return {
1647
1979
  ...links,
1648
1980
  source: "sample",
1649
1981
  sample: item.sample,
1650
1982
  product: null,
1651
- name: item.sample.name,
1983
+ name: sampleNameText,
1652
1984
  href: null,
1653
1985
  isSample: true
1654
1986
  };
@@ -1686,6 +2018,7 @@ function useProductCard() {
1686
2018
  function ProductCardRoot({ children, product, className }) {
1687
2019
  const catalogItem = useContext(CatalogItemContext);
1688
2020
  const { config } = useCommerce();
2021
+ const text = useMessage();
1689
2022
  const [imageLinked, setImageLinked] = useState(false);
1690
2023
  const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
1691
2024
  useEffect(() => {
@@ -1696,16 +2029,18 @@ function ProductCardRoot({ children, product, className }) {
1696
2029
  source: "live",
1697
2030
  product
1698
2031
  } : null);
2032
+ const sampleText = item?.source === "sample" ? sampleName(text, item.sample) : "";
1699
2033
  return item && cardValue(item, config.productPath, {
1700
2034
  imageLinked,
1701
2035
  register
1702
- });
2036
+ }, sampleText);
1703
2037
  }, [
1704
2038
  catalogItem,
1705
2039
  product,
1706
2040
  config.productPath,
1707
2041
  imageLinked,
1708
- register
2042
+ register,
2043
+ text
1709
2044
  ]);
1710
2045
  if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
1711
2046
  if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
@@ -1774,11 +2109,11 @@ function ProductCardDescription({ className }) {
1774
2109
  })
1775
2110
  });
1776
2111
  }
1777
- const formatRange = (range, money$1) => {
2112
+ const formatRange = (range, money$1, moneyRange$1) => {
1778
2113
  const min = range?.min;
1779
2114
  const max = range?.max;
1780
2115
  if (typeof min !== "number") return null;
1781
- return `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}`;
2116
+ return typeof max === "number" && max !== min ? moneyRange$1(min, max) : money$1(min);
1782
2117
  };
1783
2118
  /**
1784
2119
  * "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
@@ -1786,12 +2121,13 @@ const formatRange = (range, money$1) => {
1786
2121
  */
1787
2122
  function ProductCardBadge({ className }) {
1788
2123
  const { product } = useProductCardInternal("ProductCardBadge");
2124
+ const text = useMessage();
1789
2125
  if (!isProductOnSale(product)) return null;
1790
2126
  return /* @__PURE__ */ jsx("div", {
1791
2127
  ...partProps("product-card-badge", className),
1792
2128
  children: /* @__PURE__ */ jsx("span", {
1793
2129
  className: saleBadgeClass,
1794
- children: "Sale"
2130
+ children: text("sale")
1795
2131
  })
1796
2132
  });
1797
2133
  }
@@ -1799,7 +2135,9 @@ function ProductCardBadge({ className }) {
1799
2135
  function ProductCardPrice({ className }) {
1800
2136
  const card = useProductCardInternal("ProductCardPrice");
1801
2137
  const money$1 = useMoney();
1802
- const text = card.source === "sample" ? money$1(card.sample.price, "USD") : formatRange(card.product.priceRange, money$1) ?? "Price unavailable";
2138
+ const moneyRange$1 = useMoneyRange();
2139
+ const message = useMessage();
2140
+ const text = card.source === "sample" ? money$1(card.sample.price, sampleCurrency) : formatRange(card.product.priceRange, money$1, moneyRange$1) ?? message("priceUnavailable");
1803
2141
  const range = card.source === "live" ? card.product.priceRange : null;
1804
2142
  const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
1805
2143
  const compareAt = starting === null ? null : money$1(starting);
@@ -1812,7 +2150,7 @@ function ProductCardPrice({ className }) {
1812
2150
  children: text
1813
2151
  }), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1814
2152
  className: "sr-only",
1815
- children: "Was"
2153
+ children: message("wasPrice")
1816
2154
  }), /* @__PURE__ */ jsx("del", {
1817
2155
  className: "text-commerce-muted",
1818
2156
  children: compareAt
@@ -1827,6 +2165,7 @@ function ProductCardPrice({ className }) {
1827
2165
  function ProductCardSwatches({ className }) {
1828
2166
  const { product } = useProductCardInternal("ProductCardSwatches");
1829
2167
  const { getSwatch } = useCommerce();
2168
+ const text = useMessage();
1830
2169
  if (!getSwatch || !product) return null;
1831
2170
  for (const attribute of getProductAttributes(product)) {
1832
2171
  const swatches = attribute.values.map((value) => ({
@@ -1844,16 +2183,13 @@ function ProductCardSwatches({ className }) {
1844
2183
  children: /* @__PURE__ */ jsxs("p", {
1845
2184
  className: "flex items-center gap-1.5 text-xs text-commerce-muted",
1846
2185
  children: [
1847
- /* @__PURE__ */ jsxs("span", {
2186
+ /* @__PURE__ */ jsx("span", {
1848
2187
  className: "sr-only",
1849
- children: [
1850
- swatches.length,
1851
- " ",
1852
- inSentence(attribute.label),
1853
- " options:",
1854
- " ",
1855
- swatches.map((swatch) => swatch.label).join(", ")
1856
- ]
2188
+ children: text("swatchSummary", {
2189
+ count: swatches.length,
2190
+ attribute: inSentence(attribute.label),
2191
+ values: swatches.map((swatch) => swatch.label).join(", ")
2192
+ })
1857
2193
  }),
1858
2194
  shown.map((swatch) => /* @__PURE__ */ jsx("span", {
1859
2195
  "aria-hidden": "true",
@@ -1892,6 +2228,7 @@ function ProductCardAction({ className, children }) {
1892
2228
  function LiveAction({ product, name, href, label }) {
1893
2229
  const selectedSku = getSingleMatchedSku(product);
1894
2230
  const hasVariants = getProductAttributes(product).length > 0;
2231
+ const text = useMessage();
1895
2232
  if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
1896
2233
  sku: selectedSku,
1897
2234
  name,
@@ -1910,11 +2247,13 @@ function LiveAction({ product, name, href, label }) {
1910
2247
  action,
1911
2248
  busy: false,
1912
2249
  soldOut: false
1913
- }, hasVariants ? "Choose options" : "View product")
2250
+ }, text(hasVariants ? "chooseOptions" : "viewProduct"))
1914
2251
  });
1915
2252
  }
1916
2253
  function SampleAction({ sample, label }) {
1917
2254
  const { add } = useSampleCart();
2255
+ const text = useMessage();
2256
+ const name = sampleName(text, sample);
1918
2257
  const custom = renderLabel(label, {
1919
2258
  action: "add",
1920
2259
  busy: false,
@@ -1923,20 +2262,16 @@ function SampleAction({ sample, label }) {
1923
2262
  return custom === null ? /* @__PURE__ */ jsx("button", {
1924
2263
  type: "button",
1925
2264
  className: `${secondaryButtonClass} w-full`,
1926
- "aria-label": `Add ${sample.name} to sample cart`,
2265
+ "aria-label": text("addSampleToCart", { name }),
1927
2266
  onClick: () => add(sample.key),
1928
- children: "Add to cart"
2267
+ children: text("addToCart")
1929
2268
  }) : /* @__PURE__ */ jsxs("button", {
1930
2269
  type: "button",
1931
2270
  className: `${secondaryButtonClass} w-full`,
1932
2271
  onClick: () => add(sample.key),
1933
2272
  children: [custom, /* @__PURE__ */ jsxs("span", {
1934
2273
  className: "sr-only",
1935
- children: [
1936
- " (",
1937
- sample.name,
1938
- ", sample)"
1939
- ]
2274
+ children: [" ", text("sampleItemName", { name })]
1940
2275
  })]
1941
2276
  });
1942
2277
  }
@@ -2075,36 +2410,44 @@ function CatalogLoader({ children, className, pageSize, urlState }) {
2075
2410
  });
2076
2411
  }
2077
2412
  /** 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 }) {
2413
+ function CatalogHeader({ title, description, className }) {
2079
2414
  useCatalogInternal("CatalogHeader");
2415
+ const text = useMessage();
2080
2416
  return /* @__PURE__ */ jsxs("header", {
2081
2417
  ...partProps("catalog-header", className),
2082
- children: [/* @__PURE__ */ jsx("h1", {
2418
+ children: [title !== null && /* @__PURE__ */ jsx("h1", {
2083
2419
  className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
2084
- children: title
2085
- }), /* @__PURE__ */ jsx("p", {
2420
+ children: title === void 0 ? text("catalogTitle") : title
2421
+ }), description !== null && /* @__PURE__ */ jsx("p", {
2086
2422
  className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
2087
- children: description
2423
+ children: description === void 0 ? text("catalogDescription") : description
2088
2424
  })]
2089
2425
  });
2090
2426
  }
2091
2427
  /** Announces loading, failure, or how many products the page shows. */
2092
2428
  function CatalogSummary({ className }) {
2093
- const catalog = useCatalogInternal("CatalogSummary");
2429
+ const { key, values } = summary(useCatalogInternal("CatalogSummary"));
2430
+ const text = useMessage();
2094
2431
  return /* @__PURE__ */ jsx("div", {
2095
2432
  ...partProps("catalog-summary", className),
2096
2433
  children: /* @__PURE__ */ jsx("p", {
2097
2434
  className: "text-sm text-commerce-muted",
2098
2435
  role: "status",
2099
- children: summary(catalog)
2436
+ children: text(key, values)
2100
2437
  })
2101
2438
  });
2102
2439
  }
2103
2440
  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"}`;
2441
+ if (catalog.source === "sample") return {
2442
+ key: "sampleCatalogSummary",
2443
+ values: { currency: sampleCurrency }
2444
+ };
2445
+ if (catalog.products.isPending) return { key: "loadingProducts" };
2446
+ if (catalog.products.isError) return { key: "productsUnavailable" };
2447
+ return {
2448
+ key: "productCount",
2449
+ values: { count: catalog.nodes.length }
2450
+ };
2108
2451
  }
2109
2452
  /** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
2110
2453
  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";
@@ -2136,20 +2479,22 @@ function CatalogProducts({ className, children }) {
2136
2479
  }
2137
2480
  })) : sampleItems, [nodes]);
2138
2481
  const products = catalog.source === "live" ? catalog.products : null;
2482
+ const { messages } = useCommerce();
2483
+ const text = useMessage();
2139
2484
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2140
2485
  products?.isError && /* @__PURE__ */ jsxs("div", {
2141
2486
  role: "alert",
2142
2487
  className: errorNoticeClass,
2143
- children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
2488
+ children: [/* @__PURE__ */ jsx("p", { children: describeError(products.error, messages) }), /* @__PURE__ */ jsx("button", {
2144
2489
  type: "button",
2145
2490
  className: errorActionClass,
2146
2491
  onClick: () => void products.refetch(),
2147
- children: "Retry products"
2492
+ children: text("retryProducts")
2148
2493
  })]
2149
2494
  }),
2150
2495
  products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
2151
2496
  className: "py-16 text-center text-commerce-muted",
2152
- children: "No products available."
2497
+ children: text("noProducts")
2153
2498
  }),
2154
2499
  /* @__PURE__ */ jsx("div", {
2155
2500
  ...partProps("catalog-products", className),
@@ -2183,10 +2528,11 @@ function CatalogEmpty({ className, children }) {
2183
2528
  const catalog = useCatalogInternal("CatalogEmpty");
2184
2529
  const register = useContext(CatalogEmptyContext)?.register;
2185
2530
  useEffect(() => register?.("empty"), [register]);
2531
+ const text = useMessage();
2186
2532
  if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
2187
2533
  return /* @__PURE__ */ jsx("div", {
2188
2534
  ...partProps("catalog-empty", className),
2189
- children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
2535
+ children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: text("noProducts") })
2190
2536
  });
2191
2537
  }
2192
2538
  /**
@@ -2194,13 +2540,14 @@ function CatalogEmpty({ className, children }) {
2194
2540
  */
2195
2541
  function CatalogPagination({ className }) {
2196
2542
  const catalog = useCatalogInternal("CatalogPagination");
2543
+ const text = useMessage();
2197
2544
  if (catalog.source === "sample") return null;
2198
2545
  const { after, goToPage } = catalog;
2199
2546
  const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
2200
2547
  const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
2201
2548
  if (!after && !hasNext) return null;
2202
2549
  return /* @__PURE__ */ jsx("nav", {
2203
- "aria-label": "Product pages",
2550
+ "aria-label": text("productPages"),
2204
2551
  ...partProps("catalog-pagination", className),
2205
2552
  children: /* @__PURE__ */ jsxs("div", {
2206
2553
  className: "flex justify-center gap-3",
@@ -2208,12 +2555,12 @@ function CatalogPagination({ className }) {
2208
2555
  type: "button",
2209
2556
  className: secondaryButtonClass,
2210
2557
  onClick: () => goToPage(),
2211
- children: "First page"
2558
+ children: text("firstPage")
2212
2559
  }), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
2213
2560
  type: "button",
2214
2561
  className: secondaryButtonClass,
2215
2562
  onClick: () => goToPage(pageInfo.endCursor ?? void 0),
2216
- children: "Next page"
2563
+ children: text("nextPage")
2217
2564
  })]
2218
2565
  })
2219
2566
  });
@@ -2325,7 +2672,8 @@ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
2325
2672
  }) });
2326
2673
  }
2327
2674
  function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
2328
- const { config, getSwatch } = useCommerce();
2675
+ const { config, getSwatch, messages } = useCommerce();
2676
+ const text = useMessage();
2329
2677
  const [params, setParams] = useRootParams(urlState);
2330
2678
  const [quantity, setQuantity] = useState(1);
2331
2679
  const [imageIndex, setImageIndex] = useState(0);
@@ -2403,7 +2751,7 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2403
2751
  children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
2404
2752
  role: "status",
2405
2753
  className: "text-sm text-commerce-muted",
2406
- children: "Loading product…"
2754
+ children: text("loadingProduct")
2407
2755
  })
2408
2756
  });
2409
2757
  if (product.isError && !productNotFound) {
@@ -2412,22 +2760,22 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2412
2760
  role: "alert",
2413
2761
  ...partProps("product-details-root", className),
2414
2762
  children: renderLabel(error, {
2415
- message: message(product.error),
2763
+ message: describeError(product.error, messages),
2416
2764
  retry
2417
2765
  }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
2418
2766
  /* @__PURE__ */ jsx("h1", {
2419
2767
  className: "text-2xl font-semibold tracking-tight",
2420
- children: "Product unavailable"
2768
+ children: text("productUnavailable")
2421
2769
  }),
2422
2770
  /* @__PURE__ */ jsx("p", {
2423
2771
  className: `my-4 ${errorNoticeClass}`,
2424
- children: message(product.error)
2772
+ children: describeError(product.error, messages)
2425
2773
  }),
2426
2774
  /* @__PURE__ */ jsx("button", {
2427
2775
  type: "button",
2428
2776
  className: buttonClass,
2429
2777
  onClick: retry,
2430
- children: "Retry product"
2778
+ children: text("retryProduct")
2431
2779
  })
2432
2780
  ] }))
2433
2781
  });
@@ -2436,11 +2784,11 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
2436
2784
  ...partProps("product-details-root", className),
2437
2785
  children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
2438
2786
  className: "text-2xl font-semibold tracking-tight",
2439
- children: "Product not found"
2787
+ children: text("productNotFound")
2440
2788
  }), /* @__PURE__ */ jsx(Link, {
2441
2789
  className: `${buttonClass} mt-6`,
2442
2790
  to: config.catalogPath,
2443
- children: "Back to shop"
2791
+ children: text("backToShop")
2444
2792
  })] })
2445
2793
  });
2446
2794
  const value = {
@@ -2539,11 +2887,11 @@ function getSoldOutValues(group, attributes, selections) {
2539
2887
  //#endregion
2540
2888
  //#region src/product-purchase.tsx
2541
2889
  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."
2890
+ incomplete: "selectAllOptions",
2891
+ checking: "checkingVariant",
2892
+ "variant-error": "variantCheckFailed",
2893
+ unavailable: "combinationUnavailable",
2894
+ "not-purchasable": "notPurchasable"
2547
2895
  };
2548
2896
  /** Above this many values, buttons wrap into a wall, so the option is a select. */
2549
2897
  const maxChoiceButtons = 8;
@@ -2608,14 +2956,11 @@ const swatchClass = "relative block h-9 w-9 overflow-hidden rounded-full border
2608
2956
  function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2609
2957
  const name = useId();
2610
2958
  const errorId = useId();
2611
- const error = missing && /* @__PURE__ */ jsxs("p", {
2959
+ const text = useMessage();
2960
+ const error = missing && /* @__PURE__ */ jsx("p", {
2612
2961
  id: errorId,
2613
2962
  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
- ]
2963
+ children: text("selectAttributeError", { attribute: inSentence(label) })
2619
2964
  });
2620
2965
  if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
2621
2966
  /* @__PURE__ */ jsx("legend", {
@@ -2628,12 +2973,12 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2628
2973
  "aria-describedby": missing ? errorId : void 0,
2629
2974
  value: selected,
2630
2975
  onChange: (event) => onSelect(event.target.value),
2631
- children: [/* @__PURE__ */ jsxs("option", {
2976
+ children: [/* @__PURE__ */ jsx("option", {
2632
2977
  value: "",
2633
- children: ["Select ", inSentence(label)]
2978
+ children: text("selectAttribute", { attribute: inSentence(label) })
2634
2979
  }), choices.map((choice) => /* @__PURE__ */ jsx("option", {
2635
2980
  value: choice.value,
2636
- children: choice.soldOut ? `${choice.label} (sold out)` : choice.label
2981
+ children: choice.soldOut ? text("soldOutOption", { label: choice.label }) : choice.label
2637
2982
  }, choice.value))]
2638
2983
  }),
2639
2984
  error
@@ -2654,7 +2999,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2654
2999
  className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
2655
3000
  children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
2656
3001
  className: "group relative cursor-pointer",
2657
- title: swatches ? `${choice.label}${choice.soldOut ? ", sold out" : ""}` : void 0,
3002
+ title: swatches ? choice.soldOut ? text("soldOutChoice", { label: choice.label }) : choice.label : void 0,
2658
3003
  children: [/* @__PURE__ */ jsx("input", {
2659
3004
  type: "radio",
2660
3005
  className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
@@ -2662,7 +3007,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2662
3007
  value: choice.value,
2663
3008
  checked: choice.value === selected,
2664
3009
  "aria-invalid": missing || void 0,
2665
- "aria-label": choice.soldOut ? `${choice.label}, sold out` : void 0,
3010
+ "aria-label": choice.soldOut ? text("soldOutChoice", { label: choice.label }) : void 0,
2666
3011
  "data-commerce-control": "choice",
2667
3012
  onChange: () => onSelect(choice.value)
2668
3013
  }), swatches ? /* @__PURE__ */ jsxs("span", {
@@ -2688,12 +3033,13 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
2688
3033
  function SkuOption() {
2689
3034
  const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
2690
3035
  useSkuOptionRegistration();
3036
+ const text = useMessage();
2691
3037
  const missing = showMissing && !explicitSkuId;
2692
3038
  return /* @__PURE__ */ jsx("div", {
2693
3039
  ...partProps("product-details-sku-option"),
2694
3040
  "data-commerce-missing": missing || void 0,
2695
3041
  children: /* @__PURE__ */ jsx(ChoiceGroup, {
2696
- label: "Option",
3042
+ label: text("skuOptionLabel"),
2697
3043
  selected: explicitSkuId,
2698
3044
  missing,
2699
3045
  onSelect: selectSku,
@@ -2737,6 +3083,8 @@ function ProductDetailsOption({ attribute, className }) {
2737
3083
  */
2738
3084
  function ProductDetailsPurchaseStatus({ className }) {
2739
3085
  const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
3086
+ const { messages } = useCommerce();
3087
+ const text = useMessage();
2740
3088
  let content;
2741
3089
  switch (status) {
2742
3090
  case "ready":
@@ -2745,11 +3093,11 @@ function ProductDetailsPurchaseStatus({ className }) {
2745
3093
  content = /* @__PURE__ */ jsxs("div", {
2746
3094
  role: "alert",
2747
3095
  className: errorNoticeClass,
2748
- children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
3096
+ children: [/* @__PURE__ */ jsx("p", { children: describeError(variantError, messages) }), /* @__PURE__ */ jsx("button", {
2749
3097
  type: "button",
2750
3098
  className: errorActionClass,
2751
3099
  onClick: retryVariant,
2752
- children: "Retry availability"
3100
+ children: text("retryAvailability")
2753
3101
  })]
2754
3102
  });
2755
3103
  break;
@@ -2757,20 +3105,20 @@ function ProductDetailsPurchaseStatus({ className }) {
2757
3105
  content = /* @__PURE__ */ jsx("p", {
2758
3106
  role: "status",
2759
3107
  className: errorNoticeClass,
2760
- children: disabledReasons[status]
3108
+ children: text(disabledReasons[status])
2761
3109
  });
2762
3110
  break;
2763
3111
  case "not-purchasable":
2764
3112
  content = /* @__PURE__ */ jsx("p", {
2765
3113
  className: "text-sm text-commerce-muted",
2766
- children: disabledReasons[status]
3114
+ children: text(disabledReasons[status])
2767
3115
  });
2768
3116
  break;
2769
3117
  case "checking":
2770
3118
  content = /* @__PURE__ */ jsx("p", {
2771
3119
  role: "status",
2772
3120
  className: "text-sm text-commerce-muted",
2773
- children: disabledReasons[status]
3121
+ children: text(disabledReasons[status])
2774
3122
  });
2775
3123
  break;
2776
3124
  }
@@ -2782,13 +3130,14 @@ function ProductDetailsPurchaseStatus({ className }) {
2782
3130
  /** Stock for the verified SKU. Renders nothing until a SKU is verified. */
2783
3131
  function ProductDetailsAvailability({ className }) {
2784
3132
  const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
3133
+ const text = useMessage();
2785
3134
  if (!selectedSku) return null;
2786
3135
  return /* @__PURE__ */ jsx("div", {
2787
3136
  ...partProps("product-details-availability", className),
2788
3137
  children: /* @__PURE__ */ jsx("p", {
2789
3138
  className: "text-sm text-commerce-muted",
2790
3139
  "aria-live": "polite",
2791
- children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
3140
+ children: availableQuantity === null ? text("availableToOrder") : isSoldOut(availableQuantity) ? text("variantOutOfStock") : text("availableCount", { count: availableQuantity })
2792
3141
  })
2793
3142
  });
2794
3143
  }
@@ -2815,9 +3164,11 @@ function ProductDetailsQuantity({ className }) {
2815
3164
  const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
2816
3165
  const id = useId();
2817
3166
  const problemId = useId();
3167
+ const text = useMessage();
2818
3168
  const soldOut = !!selectedSku && isSoldOut(availableQuantity);
2819
3169
  const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
2820
- const problem = soldOut ? null : quantityProblem(quantity, max);
3170
+ const issue = soldOut ? null : quantityProblem(quantity, max);
3171
+ const problem = issue && text(issue.key, issue.values);
2821
3172
  const current = Number.isInteger(quantity) ? quantity : 1;
2822
3173
  return /* @__PURE__ */ jsxs("div", {
2823
3174
  ...partProps("product-details-quantity", className),
@@ -2825,7 +3176,7 @@ function ProductDetailsQuantity({ className }) {
2825
3176
  /* @__PURE__ */ jsx("label", {
2826
3177
  htmlFor: id,
2827
3178
  className: "sr-only",
2828
- children: "Quantity"
3179
+ children: text("quantity")
2829
3180
  }),
2830
3181
  /* @__PURE__ */ jsxs("div", {
2831
3182
  className: "inline-flex items-center rounded-lg border border-commerce-edge",
@@ -2833,7 +3184,7 @@ function ProductDetailsQuantity({ className }) {
2833
3184
  /* @__PURE__ */ jsx("button", {
2834
3185
  type: "button",
2835
3186
  className: stepperButtonClass,
2836
- "aria-label": `Decrease quantity of ${name}`,
3187
+ "aria-label": text("decreaseQuantity", { name }),
2837
3188
  "data-commerce-control": "decrease",
2838
3189
  disabled: soldOut || current <= 1,
2839
3190
  onClick: () => setQuantity(current - 1),
@@ -2855,7 +3206,7 @@ function ProductDetailsQuantity({ className }) {
2855
3206
  /* @__PURE__ */ jsx("button", {
2856
3207
  type: "button",
2857
3208
  className: stepperButtonClass,
2858
- "aria-label": `Increase quantity of ${name}`,
3209
+ "aria-label": text("increaseQuantity", { name }),
2859
3210
  "data-commerce-control": "increase",
2860
3211
  disabled: soldOut || max !== null && current >= max,
2861
3212
  onClick: () => setQuantity(current + 1),
@@ -2880,6 +3231,7 @@ function ProductDetailsAddToCart({ className, children }) {
2880
3231
  const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
2881
3232
  const { config } = useCommerce();
2882
3233
  const money$1 = useMoney();
3234
+ const text = useMessage();
2883
3235
  const reasonId = useId();
2884
3236
  if (selectedSku) {
2885
3237
  const unit = price?.value?.value;
@@ -2892,8 +3244,8 @@ function ProductDetailsAddToCart({ className, children }) {
2892
3244
  quantity,
2893
3245
  className: buttonClass,
2894
3246
  block: true,
2895
- label: total && `Add to cart · ${total}`,
2896
- labelName: total ? `Add to cart, ${total}` : void 0,
3247
+ label: total && text("addToCartWithPrice", { price: total }),
3248
+ labelName: total ? text("addToCartWithPriceName", { price: total }) : void 0,
2897
3249
  children
2898
3250
  })
2899
3251
  });
@@ -2910,7 +3262,7 @@ function ProductDetailsAddToCart({ className, children }) {
2910
3262
  children: renderLabel(children, {
2911
3263
  busy: false,
2912
3264
  soldOut: false
2913
- }, next ? `Select ${inSentence(next)}` : "Select an option")
3265
+ }, next ? text("selectAttribute", { attribute: inSentence(next) }) : text("selectOption"))
2914
3266
  })
2915
3267
  });
2916
3268
  }
@@ -2925,11 +3277,11 @@ function ProductDetailsAddToCart({ className, children }) {
2925
3277
  children: renderLabel(children, {
2926
3278
  busy: false,
2927
3279
  soldOut: false
2928
- }, "Add to cart")
3280
+ }, text("addToCart"))
2929
3281
  }), /* @__PURE__ */ jsx("span", {
2930
3282
  id: reasonId,
2931
3283
  hidden: true,
2932
- children: status === "ready" ? "" : disabledReasons[status]
3284
+ children: status === "ready" ? "" : text(disabledReasons[status])
2933
3285
  })]
2934
3286
  });
2935
3287
  }
@@ -2958,6 +3310,7 @@ function ProductDetails() {
2958
3310
  function ProductDetailsBackLink({ className, children }) {
2959
3311
  useProductDetailsInternal("ProductDetailsBackLink");
2960
3312
  const { config } = useCommerce();
3313
+ const text = useMessage();
2961
3314
  return /* @__PURE__ */ jsx("div", {
2962
3315
  ...partProps("product-details-back-link", className),
2963
3316
  children: /* @__PURE__ */ jsx(Link, {
@@ -2975,13 +3328,14 @@ function ProductDetailsBackLink({ className, children }) {
2975
3328
  strokeLinecap: "round",
2976
3329
  strokeLinejoin: "round"
2977
3330
  })
2978
- }), "Back to shop"] }))
3331
+ }), text("backToShop")] }))
2979
3332
  })
2980
3333
  });
2981
3334
  }
2982
3335
  /** Main image with thumbnails; the selected SKU's images replace the product's when present. */
2983
3336
  function ProductDetailsGallery({ className }) {
2984
3337
  const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
3338
+ const text = useMessage();
2985
3339
  return /* @__PURE__ */ jsxs("div", {
2986
3340
  ...partProps("product-details-gallery", className),
2987
3341
  children: [/* @__PURE__ */ jsx("div", {
@@ -2995,7 +3349,7 @@ function ProductDetailsGallery({ className }) {
2995
3349
  children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
2996
3350
  type: "button",
2997
3351
  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}`,
3352
+ "aria-label": text("viewImage", { number: index + 1 }),
2999
3353
  "aria-pressed": imageIndex === index,
3000
3354
  "data-commerce-control": "thumbnail",
3001
3355
  onClick: () => setImageIndex(index),
@@ -3013,12 +3367,13 @@ function ProductDetailsGallery({ className }) {
3013
3367
  */
3014
3368
  function ProductDetailsBadge({ className }) {
3015
3369
  const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
3370
+ const text = useMessage();
3016
3371
  if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
3017
3372
  return /* @__PURE__ */ jsx("div", {
3018
3373
  ...partProps("product-details-badge", className),
3019
3374
  children: /* @__PURE__ */ jsx("span", {
3020
3375
  className: saleBadgeClass,
3021
- children: "Sale"
3376
+ children: text("sale")
3022
3377
  })
3023
3378
  });
3024
3379
  }
@@ -3041,6 +3396,7 @@ function ProductDetailsPrice({ className }) {
3041
3396
  const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
3042
3397
  const { config } = useCommerce();
3043
3398
  const money$1 = useMoney();
3399
+ const text = useMessage();
3044
3400
  let compareAt = null;
3045
3401
  if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
3046
3402
  else if (!price) {
@@ -3053,9 +3409,9 @@ function ProductDetailsPrice({ className }) {
3053
3409
  "aria-live": "polite",
3054
3410
  className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
3055
3411
  "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", {
3412
+ 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
3413
  className: "sr-only",
3058
- children: "Was"
3414
+ children: text("wasPrice")
3059
3415
  }), /* @__PURE__ */ jsx("del", {
3060
3416
  className: "text-base font-normal text-commerce-muted",
3061
3417
  children: compareAt
@@ -3087,10 +3443,11 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
3087
3443
  * the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
3088
3444
  * fails, or while no store is connected.
3089
3445
  */
3090
- function RelatedProducts({ productId: productIdProp, title = "You may also like", limit = 4, className, children }) {
3446
+ function RelatedProducts({ productId: productIdProp, title, limit = 4, className, children }) {
3091
3447
  const { productId: routeProductId = "" } = useParams();
3092
3448
  const productId = productIdProp ?? routeProductId;
3093
3449
  const { config, connection } = useCommerce();
3450
+ const text = useMessage();
3094
3451
  const headingId = useId();
3095
3452
  const first = limit + 1;
3096
3453
  const products = useQuery({
@@ -3117,12 +3474,12 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
3117
3474
  if (connection !== "ready" || !related.length) return null;
3118
3475
  const card = hasChildren(children) ? children : packagedCard;
3119
3476
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
3120
- "aria-labelledby": headingId,
3477
+ "aria-labelledby": title === null ? void 0 : headingId,
3121
3478
  ...partProps("related-products", className),
3122
- children: [/* @__PURE__ */ jsx("h2", {
3479
+ children: [title !== null && /* @__PURE__ */ jsx("h2", {
3123
3480
  id: headingId,
3124
3481
  className: "mb-6 text-xl font-semibold tracking-tight",
3125
- children: title
3482
+ children: title === void 0 ? text("relatedTitle") : title
3126
3483
  }), /* @__PURE__ */ jsx("div", {
3127
3484
  className: catalogGridClass,
3128
3485
  children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
@@ -3137,4 +3494,4 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
3137
3494
  }
3138
3495
 
3139
3496
  //#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 };
3497
+ 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, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };