@cartbase/storefront 0.19.0 → 0.20.1

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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
@@ -1,53 +1,53 @@
1
- /**
2
- * Payment tender helpers — identifier sniffing for store payment sessions.
3
- *
4
- * The Cartbase wire has exactly TWO tender shapes since the pp_* kill
5
- * (20260811250000, no-payment-method-by-default card):
6
- *
7
- * - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
8
- * today; PayPal etc. as integrations land);
9
- * - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
10
- * `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
11
- * collection-on-delivery switch). Its session carries `provider_id`
12
- * NULL; the merchant's NAME is the display everywhere.
13
- *
14
- * The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
15
- * the wire and from this module. The Medusa-era Stripe prefixes
16
- * (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
17
- * ported later keep working against either wire.
18
- *
19
- * Intentionally ships NO icons — stores render their own icons via their
20
- * own icon set. The library stays free of icon-kit imports at this layer.
21
- */
22
-
23
- /**
24
- * True if the provider id is a Stripe-backed card payment provider.
25
- * Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
26
- * Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
27
- */
28
- export const isStripeLike = (providerId?: string | null): boolean => {
29
- return Boolean(
30
- providerId &&
31
- (providerId === "pp_stripe" ||
32
- providerId === "stripe" ||
33
- providerId.startsWith("pp_stripe_") ||
34
- providerId.startsWith("pp_medusa-"))
35
- )
36
- }
37
-
38
- /** True if the provider id is PayPal. */
39
- export const isPaypal = (providerId?: string | null): boolean => {
40
- return Boolean(providerId?.startsWith("pp_paypal"))
41
- }
42
-
43
- export const paymentInfoMap: Record<
44
- string,
45
- { title: string; icon: React.JSX.Element | null }
46
- > = {
47
- pp_stripe: { title: "Credit / debit card", icon: null },
48
- pp_stripe_stripe: { title: "Credit / debit card", icon: null },
49
- "pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
50
- "pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
51
- "pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
52
- pp_paypal_paypal: { title: "PayPal", icon: null },
53
- }
1
+ /**
2
+ * Payment tender helpers — identifier sniffing for store payment sessions.
3
+ *
4
+ * The Cartbase wire has exactly TWO tender shapes since the pp_* kill
5
+ * (20260811250000, no-payment-method-by-default card):
6
+ *
7
+ * - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
8
+ * today; PayPal etc. as integrations land);
9
+ * - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
10
+ * `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
11
+ * collection-on-delivery switch). Its session carries `provider_id`
12
+ * NULL; the merchant's NAME is the display everywhere.
13
+ *
14
+ * The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
15
+ * the wire and from this module. The Medusa-era Stripe prefixes
16
+ * (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
17
+ * ported later keep working against either wire.
18
+ *
19
+ * Intentionally ships NO icons — stores render their own icons via their
20
+ * own icon set. The library stays free of icon-kit imports at this layer.
21
+ */
22
+
23
+ /**
24
+ * True if the provider id is a Stripe-backed card payment provider.
25
+ * Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
26
+ * Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
27
+ */
28
+ export const isStripeLike = (providerId?: string | null): boolean => {
29
+ return Boolean(
30
+ providerId &&
31
+ (providerId === "pp_stripe" ||
32
+ providerId === "stripe" ||
33
+ providerId.startsWith("pp_stripe_") ||
34
+ providerId.startsWith("pp_medusa-"))
35
+ )
36
+ }
37
+
38
+ /** True if the provider id is PayPal. */
39
+ export const isPaypal = (providerId?: string | null): boolean => {
40
+ return Boolean(providerId?.startsWith("pp_paypal"))
41
+ }
42
+
43
+ export const paymentInfoMap: Record<
44
+ string,
45
+ { title: string; icon: React.JSX.Element | null }
46
+ > = {
47
+ pp_stripe: { title: "Credit / debit card", icon: null },
48
+ pp_stripe_stripe: { title: "Credit / debit card", icon: null },
49
+ "pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
50
+ "pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
51
+ "pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
52
+ pp_paypal_paypal: { title: "PayPal", icon: null },
53
+ }
@@ -1,13 +1,13 @@
1
- /**
2
- * The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
3
- * a constant, not an input. Every Cartbase store's API lives here; a
4
- * storefront overrides it only to point at a local or staging platform
5
- * (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
6
- * publishable key does.
7
- */
8
- export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
9
-
10
- /** The origin to call: the override when set, else the platform. */
11
- export function cartbaseApiOrigin(override?: string | null): string {
12
- return (override && override.trim()) || CARTBASE_API_ORIGIN
13
- }
1
+ /**
2
+ * The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
3
+ * a constant, not an input. Every Cartbase store's API lives here; a
4
+ * storefront overrides it only to point at a local or staging platform
5
+ * (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
6
+ * publishable key does.
7
+ */
8
+ export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
9
+
10
+ /** The origin to call: the override when set, else the platform. */
11
+ export function cartbaseApiOrigin(override?: string | null): string {
12
+ return (override && override.trim()) || CARTBASE_API_ORIGIN
13
+ }
package/src/lib/price.tsx CHANGED
@@ -1,39 +1,39 @@
1
- import * as React from "react"
2
-
3
- import { convertToLocale } from "./money"
4
-
5
- /**
6
- * Price — render an amount in its own currency, and nothing else.
7
- *
8
- * Replaces `DualPrice` (deleted 2026-09-13, Alexander: "clean and clear
9
- * the dual pricing"). That component appended a grey BGN leg to every EUR
10
- * price for Bulgaria's euro-changeover disclosure rule, with the statutory
11
- * 1.95583 rate baked in. Cartbase prices in EUR (see the EUR-only law) and
12
- * the second currency belongs to a store's own locale work if it ever
13
- * wants one, not to every price in the library.
14
- *
15
- * Stateless and side-effect-free, so it renders in server and client
16
- * components alike, and it is deliberately NOT a client component: the old
17
- * one carried "use client" for no reason, which pulled every price
18
- * rendering into the client bundle.
19
- *
20
- * @example
21
- * <Price amount={19.99} currencyCode="eur" /> // "€19.99"
22
- */
23
-
24
- export type PriceProps = {
25
- /** Amount in major units of the currency (19.99 for €19.99). */
26
- amount: number
27
- /** ISO currency code, case-insensitive. */
28
- currencyCode: string
29
- /** Class for the span. */
30
- className?: string
31
- }
32
-
33
- export function Price({ amount, currencyCode, className }: PriceProps): React.ReactElement {
34
- return (
35
- <span className={className}>
36
- {convertToLocale({ amount, currency_code: currencyCode })}
37
- </span>
38
- )
39
- }
1
+ import * as React from "react"
2
+
3
+ import { convertToLocale } from "./money"
4
+
5
+ /**
6
+ * Price — render an amount in its own currency, and nothing else.
7
+ *
8
+ * Replaces `DualPrice` (deleted 2026-09-13, Alexander: "clean and clear
9
+ * the dual pricing"). That component appended a grey BGN leg to every EUR
10
+ * price for Bulgaria's euro-changeover disclosure rule, with the statutory
11
+ * 1.95583 rate baked in. Cartbase prices in EUR (see the EUR-only law) and
12
+ * the second currency belongs to a store's own locale work if it ever
13
+ * wants one, not to every price in the library.
14
+ *
15
+ * Stateless and side-effect-free, so it renders in server and client
16
+ * components alike, and it is deliberately NOT a client component: the old
17
+ * one carried "use client" for no reason, which pulled every price
18
+ * rendering into the client bundle.
19
+ *
20
+ * @example
21
+ * <Price amount={19.99} currencyCode="eur" /> // "€19.99"
22
+ */
23
+
24
+ export type PriceProps = {
25
+ /** Amount in major units of the currency (19.99 for €19.99). */
26
+ amount: number
27
+ /** ISO currency code, case-insensitive. */
28
+ currencyCode: string
29
+ /** Class for the span. */
30
+ className?: string
31
+ }
32
+
33
+ export function Price({ amount, currencyCode, className }: PriceProps): React.ReactElement {
34
+ return (
35
+ <span className={className}>
36
+ {convertToLocale({ amount, currency_code: currencyCode })}
37
+ </span>
38
+ )
39
+ }
@@ -1,36 +1,36 @@
1
- import { StoreApiError } from "../api/types"
2
-
3
- /**
4
- * storeApiError — the successor of `@1click/ui`'s `medusaError`.
5
- *
6
- * Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
7
- * clean, user-presentable `Error` whose message starts with a capital and
8
- * ends with a period — the same normalization contract the Medusa-era
9
- * helper enforced, adapted to the Cartbase error contract (`StoreApiError`
10
- * with `status` + `code` + raw `body`, see `../api/types`).
11
- *
12
- * Always throws — never returns. Use in `.catch()` chains:
13
- *
14
- * addLineItem(client, cartId, input).catch(storeApiError)
15
- *
16
- * Callers that need the PROGRAMMATIC surface (branching on `status` /
17
- * `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
18
- * `StoreApiError` directly instead — this adapter is the display-boundary
19
- * normalizer, deliberately lossy about everything but the message.
20
- */
21
- export default function storeApiError(error: unknown): never {
22
- if (error instanceof StoreApiError) {
23
- const message = error.message || `Store API error (${error.status})`
24
- throw new Error(sentence(message))
25
- }
26
- if (error instanceof Error) {
27
- throw new Error(sentence(error.message || "Unknown error"))
28
- }
29
- throw new Error("Error setting up the request: " + String(error))
30
- }
31
-
32
- /** Capitalize + terminal period — the medusaError display normalization. */
33
- function sentence(message: string): string {
34
- const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
35
- return capitalized.endsWith(".") ? capitalized : capitalized + "."
36
- }
1
+ import { StoreApiError } from "../api/types"
2
+
3
+ /**
4
+ * storeApiError — the successor of `@1click/ui`'s `medusaError`.
5
+ *
6
+ * Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
7
+ * clean, user-presentable `Error` whose message starts with a capital and
8
+ * ends with a period — the same normalization contract the Medusa-era
9
+ * helper enforced, adapted to the Cartbase error contract (`StoreApiError`
10
+ * with `status` + `code` + raw `body`, see `../api/types`).
11
+ *
12
+ * Always throws — never returns. Use in `.catch()` chains:
13
+ *
14
+ * addLineItem(client, cartId, input).catch(storeApiError)
15
+ *
16
+ * Callers that need the PROGRAMMATIC surface (branching on `status` /
17
+ * `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
18
+ * `StoreApiError` directly instead — this adapter is the display-boundary
19
+ * normalizer, deliberately lossy about everything but the message.
20
+ */
21
+ export default function storeApiError(error: unknown): never {
22
+ if (error instanceof StoreApiError) {
23
+ const message = error.message || `Store API error (${error.status})`
24
+ throw new Error(sentence(message))
25
+ }
26
+ if (error instanceof Error) {
27
+ throw new Error(sentence(error.message || "Unknown error"))
28
+ }
29
+ throw new Error("Error setting up the request: " + String(error))
30
+ }
31
+
32
+ /** Capitalize + terminal period — the medusaError display normalization. */
33
+ function sentence(message: string): string {
34
+ const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
35
+ return capitalized.endsWith(".") ? capitalized : capitalized + "."
36
+ }
@@ -1,32 +1,32 @@
1
- /**
2
- * The variant caption a shopper sees under a product title.
3
- *
4
- * A product with one variant has no real variant, only a placeholder name
5
- * for the row that holds its price: the platform names it "Default", a
6
- * Shopify import carries "Default Title", Medusa "Default variant". None of
7
- * them is a fact about the product, so none is shown. Every surface that
8
- * prints a variant (cart drawer, checkout summary, order page) goes through
9
- * this one function; a real variant title ("Size M / Red") passes through.
10
- *
11
- * Found 2026-09-14 on evoo's checkout summary, where every Shopify-imported
12
- * product read "Default Title" under its name while three surfaces each
13
- * tested a different spelling of the placeholder.
14
- */
15
-
16
- const PLACEHOLDER_TITLES = new Set(["default", "default title", "default variant"])
17
-
18
- /** True for an empty title or any spelling of the single-variant placeholder. */
19
- export function isPlaceholderVariantTitle(
20
- title: string | null | undefined
21
- ): boolean {
22
- if (!title) return true
23
- return PLACEHOLDER_TITLES.has(title.trim().toLowerCase())
24
- }
25
-
26
- /** The caption to render, or null when there is nothing worth showing. */
27
- export function variantCaption(title: string | null | undefined): string | null {
28
- if (!title) return null
29
- const trimmed = title.trim()
30
- if (!trimmed || isPlaceholderVariantTitle(trimmed)) return null
31
- return trimmed
32
- }
1
+ /**
2
+ * The variant caption a shopper sees under a product title.
3
+ *
4
+ * A product with one variant has no real variant, only a placeholder name
5
+ * for the row that holds its price: the platform names it "Default", a
6
+ * Shopify import carries "Default Title", Medusa "Default variant". None of
7
+ * them is a fact about the product, so none is shown. Every surface that
8
+ * prints a variant (cart drawer, checkout summary, order page) goes through
9
+ * this one function; a real variant title ("Size M / Red") passes through.
10
+ *
11
+ * Found 2026-09-14 on evoo's checkout summary, where every Shopify-imported
12
+ * product read "Default Title" under its name while three surfaces each
13
+ * tested a different spelling of the placeholder.
14
+ */
15
+
16
+ const PLACEHOLDER_TITLES = new Set(["default", "default title", "default variant"])
17
+
18
+ /** True for an empty title or any spelling of the single-variant placeholder. */
19
+ export function isPlaceholderVariantTitle(
20
+ title: string | null | undefined
21
+ ): boolean {
22
+ if (!title) return true
23
+ return PLACEHOLDER_TITLES.has(title.trim().toLowerCase())
24
+ }
25
+
26
+ /** The caption to render, or null when there is nothing worth showing. */
27
+ export function variantCaption(title: string | null | undefined): string | null {
28
+ if (!title) return null
29
+ const trimmed = title.trim()
30
+ if (!trimmed || isPlaceholderVariantTitle(trimmed)) return null
31
+ return trimmed
32
+ }
package/src/locales/bg.ts CHANGED
@@ -374,14 +374,14 @@ export const bg: StorefrontLocale = {
374
374
  sectionTitle: "Отзиви",
375
375
  reviewSingular: "отзив",
376
376
  reviewPlural: "отзива",
377
- sortAria: "Сортирай",
377
+ sortAria: "Сортирай по",
378
378
  sortDefault: "Препоръчани",
379
379
  sortNewest: "Най-нови",
380
380
  sortRatingDesc: "Най-високи оценки",
381
381
  sortRatingAsc: "Най-ниски оценки",
382
382
  verified: "Потвърден",
383
383
  verifiedTitle: "Истински купувач, потвърден чрез линк от поръчка",
384
- loadMore: "Покажи още",
384
+ loadMore: "Покажи още отзиви",
385
385
  loading: "Зарежда...",
386
386
  dateToday: "днес",
387
387
  dateYesterday: "вчера",
@@ -389,6 +389,7 @@ export const bg: StorefrontLocale = {
389
389
  showPhoto: "Покажи снимка",
390
390
  playVideo: "Пусни видео",
391
391
  close: "Затвори",
392
+ ratingBreakdown: "Разбивка по оценки",
392
393
  openReview: "Отвори отзива",
393
394
  verifiedInfo: "Какво значи потвърден",
394
395
  storeReply: "Отговор от магазина",
@@ -1,37 +1,37 @@
1
- "use client"
2
-
3
- import { createContext, useContext } from "react"
4
- import type { StorefrontLocale } from "./types"
5
-
6
- /**
7
- * THE MOUNTED LANGUAGE, as a context. Null outside `StorefrontLocaleProvider`.
8
- *
9
- * Its default is null and not `en` on purpose: `en` gathers every area's
10
- * English, and a client component that needs one area (the cart drawer, a
11
- * review card) must not bundle the other five to read it. Each reader hands
12
- * over its own area's English instead, through `useLocaleArea`, so a
13
- * component rendered with no language mounted behaves exactly as before.
14
- */
15
- export const StorefrontLocaleContext = createContext<StorefrontLocale | null>(null)
16
-
17
- /**
18
- * One area of the mounted language, for a CLIENT component that renders it.
19
- *
20
- * const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
21
- *
22
- * The pack the store mounted wins over the area's English; a `labels` prop
23
- * still wins over both, so a store can override one word without a pack.
24
- * Since 2026-09-14 this is how the cart drawer and the reviews family are
25
- * localized: mount the provider once and there is nothing left to forget.
26
- *
27
- * A SERVER component cannot call this, because a server component has no
28
- * context. That is why the store, product and order templates take their
29
- * pack as a REQUIRED prop instead (`PropLabelAreas` in ./types).
30
- */
31
- export function useLocaleArea<K extends keyof StorefrontLocale>(
32
- area: K,
33
- english: StorefrontLocale[K]
34
- ): StorefrontLocale[K] {
35
- const locale = useContext(StorefrontLocaleContext)
36
- return locale ? locale[area] : english
37
- }
1
+ "use client"
2
+
3
+ import { createContext, useContext } from "react"
4
+ import type { StorefrontLocale } from "./types"
5
+
6
+ /**
7
+ * THE MOUNTED LANGUAGE, as a context. Null outside `StorefrontLocaleProvider`.
8
+ *
9
+ * Its default is null and not `en` on purpose: `en` gathers every area's
10
+ * English, and a client component that needs one area (the cart drawer, a
11
+ * review card) must not bundle the other five to read it. Each reader hands
12
+ * over its own area's English instead, through `useLocaleArea`, so a
13
+ * component rendered with no language mounted behaves exactly as before.
14
+ */
15
+ export const StorefrontLocaleContext = createContext<StorefrontLocale | null>(null)
16
+
17
+ /**
18
+ * One area of the mounted language, for a CLIENT component that renders it.
19
+ *
20
+ * const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
21
+ *
22
+ * The pack the store mounted wins over the area's English; a `labels` prop
23
+ * still wins over both, so a store can override one word without a pack.
24
+ * Since 2026-09-14 this is how the cart drawer and the reviews family are
25
+ * localized: mount the provider once and there is nothing left to forget.
26
+ *
27
+ * A SERVER component cannot call this, because a server component has no
28
+ * context. That is why the store, product and order templates take their
29
+ * pack as a REQUIRED prop instead (`PropLabelAreas` in ./types).
30
+ */
31
+ export function useLocaleArea<K extends keyof StorefrontLocale>(
32
+ area: K,
33
+ english: StorefrontLocale[K]
34
+ ): StorefrontLocale[K] {
35
+ const locale = useContext(StorefrontLocaleContext)
36
+ return locale ? locale[area] : english
37
+ }
package/src/locales/en.ts CHANGED
@@ -1,26 +1,26 @@
1
- import { defaultCartDrawerLabels } from "../cart-drawer/labels"
2
- import { defaultCheckoutLabels } from "../checkout/labels"
3
- import { defaultOrderLabels } from "../order/labels"
4
- import { defaultProductLabels } from "../products/labels"
5
- import { defaultReviewsUiLabels } from "../reviews-ui/labels"
6
- import { defaultStoreLabels } from "../store/labels"
7
- import type { StorefrontLocale } from "./types"
8
-
9
- /**
10
- * ENGLISH — the library's own default, gathered into one locale object.
11
- *
12
- * It holds NO copy of its own: every field points at the defaults that
13
- * live beside each area's type, which are what a component renders when no
14
- * provider is mounted. So English cannot drift from the library, and a new
15
- * label added to any area appears here with no edit.
16
- */
17
- export const en: StorefrontLocale = {
18
- code: "en",
19
- name: "English",
20
- products: defaultProductLabels,
21
- store: defaultStoreLabels,
22
- cartDrawer: defaultCartDrawerLabels,
23
- checkout: defaultCheckoutLabels,
24
- order: defaultOrderLabels,
25
- reviews: defaultReviewsUiLabels,
26
- }
1
+ import { defaultCartDrawerLabels } from "../cart-drawer/labels"
2
+ import { defaultCheckoutLabels } from "../checkout/labels"
3
+ import { defaultOrderLabels } from "../order/labels"
4
+ import { defaultProductLabels } from "../products/labels"
5
+ import { defaultReviewsUiLabels } from "../reviews-ui/labels"
6
+ import { defaultStoreLabels } from "../store/labels"
7
+ import type { StorefrontLocale } from "./types"
8
+
9
+ /**
10
+ * ENGLISH — the library's own default, gathered into one locale object.
11
+ *
12
+ * It holds NO copy of its own: every field points at the defaults that
13
+ * live beside each area's type, which are what a component renders when no
14
+ * provider is mounted. So English cannot drift from the library, and a new
15
+ * label added to any area appears here with no edit.
16
+ */
17
+ export const en: StorefrontLocale = {
18
+ code: "en",
19
+ name: "English",
20
+ products: defaultProductLabels,
21
+ store: defaultStoreLabels,
22
+ cartDrawer: defaultCartDrawerLabels,
23
+ checkout: defaultCheckoutLabels,
24
+ order: defaultOrderLabels,
25
+ reviews: defaultReviewsUiLabels,
26
+ }
package/src/locales/es.ts CHANGED
@@ -384,14 +384,14 @@ export const es: StorefrontLocale = {
384
384
  sectionTitle: "Valoraciones",
385
385
  reviewSingular: "valoración",
386
386
  reviewPlural: "valoraciones",
387
- sortAria: "Ordenar",
387
+ sortAria: "Ordenar por",
388
388
  sortDefault: "Recomendadas",
389
389
  sortNewest: "Más recientes",
390
390
  sortRatingDesc: "Mejor valoradas",
391
391
  sortRatingAsc: "Peor valoradas",
392
392
  verified: "Compra verificada",
393
393
  verifiedTitle: "Comprador real, verificado mediante un enlace de pedido",
394
- loadMore: "Mostrar más",
394
+ loadMore: "Mostrar más valoraciones",
395
395
  loading: "Cargando...",
396
396
  dateToday: "hoy",
397
397
  dateYesterday: "ayer",
@@ -399,6 +399,7 @@ export const es: StorefrontLocale = {
399
399
  showPhoto: "Ver foto",
400
400
  playVideo: "Reproducir vídeo",
401
401
  close: "Cerrar",
402
+ ratingBreakdown: "Desglose de valoraciones",
402
403
  openReview: "Abrir la valoración",
403
404
  verifiedInfo: "Qué significa compra verificada",
404
405
  storeReply: "Respuesta de la tienda",
@@ -1,19 +1,19 @@
1
- /**
2
- * THE LOCALE LAYER.
3
- *
4
- * The library speaks international ENGLISH by default and knows nothing
5
- * about any other language. A language is a PACK the store imports:
6
- *
7
- * import { es } from "@cartbase/storefront/locales/es"
8
- * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
9
- *
10
- * Import a pack by its own subpath (`/locales/es`, `/locales/bg`) rather
11
- * than from here, so a store that ships one language does not bundle the
12
- * others. This barrel carries the type, the provider and English only.
13
- *
14
- * Shipping: English, Spanish, Bulgarian (2026-09-13). Adding a language is
15
- * one file in this folder plus its export path, and nothing else.
16
- */
17
- export { en } from "./en"
18
- export { StorefrontLocaleProvider, useStorefrontLocale } from "./provider"
19
- export type { PropLabelAreas, StorefrontLocale } from "./types"
1
+ /**
2
+ * THE LOCALE LAYER.
3
+ *
4
+ * The library speaks international ENGLISH by default and knows nothing
5
+ * about any other language. A language is a PACK the store imports:
6
+ *
7
+ * import { es } from "@cartbase/storefront/locales/es"
8
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
9
+ *
10
+ * Import a pack by its own subpath (`/locales/es`, `/locales/bg`) rather
11
+ * than from here, so a store that ships one language does not bundle the
12
+ * others. This barrel carries the type, the provider and English only.
13
+ *
14
+ * Shipping: English, Spanish, Bulgarian (2026-09-13). Adding a language is
15
+ * one file in this folder plus its export path, and nothing else.
16
+ */
17
+ export { en } from "./en"
18
+ export { StorefrontLocaleProvider, useStorefrontLocale } from "./provider"
19
+ export type { PropLabelAreas, StorefrontLocale } from "./types"