@cartbase/storefront 0.20.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 (99) 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/search.ts +163 -163
  20. package/src/api/store.ts +35 -35
  21. package/src/api/types.ts +91 -91
  22. package/src/cart-drawer/context.tsx +778 -778
  23. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  24. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  25. package/src/cart-drawer/gift-wrap.tsx +82 -82
  26. package/src/cart-drawer/item/index.tsx +162 -162
  27. package/src/cart-drawer/item/upsell.tsx +110 -110
  28. package/src/cart-drawer/labels.ts +123 -123
  29. package/src/cart-drawer/mutation-queue.ts +78 -78
  30. package/src/cart-drawer/notes.tsx +131 -131
  31. package/src/cart-drawer/sticky-footer.tsx +73 -73
  32. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  33. package/src/checkout/address-error-copy.ts +117 -117
  34. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  35. package/src/checkout/compare-addresses.ts +40 -40
  36. package/src/checkout/discount-section.tsx +218 -218
  37. package/src/checkout/error-copy-codes.ts +63 -63
  38. package/src/checkout/geocode.ts +154 -154
  39. package/src/checkout/gift-card-section.tsx +224 -224
  40. package/src/checkout/index.ts +72 -72
  41. package/src/checkout/labels.ts +495 -495
  42. package/src/checkout/payment-button.tsx +372 -372
  43. package/src/checkout/payment-error-copy.ts +154 -154
  44. package/src/checkout/promotion-error-copy.ts +91 -91
  45. package/src/common/country-flag.tsx +52 -52
  46. package/src/common/country-select.tsx +11 -11
  47. package/src/common/index.ts +20 -20
  48. package/src/common/market-select.tsx +57 -57
  49. package/src/index.ts +12 -12
  50. package/src/lib/country-name.ts +59 -59
  51. package/src/lib/get-product-price.ts +133 -133
  52. package/src/lib/media-image.tsx +39 -39
  53. package/src/lib/payment-constants.ts +53 -53
  54. package/src/lib/platform.ts +13 -13
  55. package/src/lib/price.tsx +39 -39
  56. package/src/lib/store-api-error.ts +36 -36
  57. package/src/lib/variant-caption.ts +32 -32
  58. package/src/locales/context.ts +37 -37
  59. package/src/locales/en.ts +26 -26
  60. package/src/locales/index.ts +19 -19
  61. package/src/locales/provider.tsx +59 -59
  62. package/src/locales/types.ts +77 -77
  63. package/src/order/index.ts +62 -62
  64. package/src/order/labels.ts +79 -79
  65. package/src/order/order-totals.tsx +250 -250
  66. package/src/primitives/select-field.tsx +93 -93
  67. package/src/products/image-gallery.tsx +43 -43
  68. package/src/products/option-select.tsx +67 -67
  69. package/src/products/product-info.tsx +5 -5
  70. package/src/products/product-price.tsx +69 -69
  71. package/src/products/product-promises.tsx +61 -61
  72. package/src/products/product-specs.tsx +65 -65
  73. package/src/products/product-tabs.tsx +123 -123
  74. package/src/products/purchase-options.tsx +130 -130
  75. package/src/products/sets.ts +110 -110
  76. package/src/products/variant-matching.ts +71 -71
  77. package/src/products/variant-url.ts +74 -74
  78. package/src/reviews-ui/lightbox-state.ts +46 -46
  79. package/src/reviews-ui/review-header.tsx +166 -166
  80. package/src/reviews-ui/review-lightbox.tsx +271 -271
  81. package/src/reviews-ui/review-list.tsx +193 -193
  82. package/src/reviews-ui/review-widget.tsx +219 -218
  83. package/src/reviews-ui/widget-options.ts +55 -55
  84. package/src/store/category-template.tsx +136 -136
  85. package/src/store/index.ts +40 -40
  86. package/src/tracking/chatgpt-pixel.tsx +99 -99
  87. package/src/tracking/consent-init.tsx +62 -62
  88. package/src/tracking/events.ts +348 -348
  89. package/src/tracking/ga4.tsx +93 -93
  90. package/src/tracking/google-ads.ts +84 -84
  91. package/src/tracking/gtm.tsx +60 -60
  92. package/src/tracking/inline-script.ts +49 -49
  93. package/src/tracking/oaiq.ts +206 -206
  94. package/src/tracking/tiktok-pixel.tsx +91 -91
  95. package/src/tracking/track-init.tsx +56 -56
  96. package/src/tracking/track-order-purchase.tsx +122 -122
  97. package/src/tracking/ttq.ts +180 -180
  98. package/src/tracking/use-tracking-config.ts +54 -54
  99. 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
+ }
@@ -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
+ }
@@ -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"
@@ -1,59 +1,59 @@
1
- "use client"
2
-
3
- import { useContext, type ReactNode } from "react"
4
- import { CheckoutProvider } from "../checkout/context"
5
- import { OrderLabelsProvider } from "../order/context"
6
- import { ProductLabelsProvider } from "../products/context"
7
- import { StorefrontLocaleContext } from "./context"
8
- import type { StorefrontLocale } from "./types"
9
- import { en } from "./en"
10
-
11
- /**
12
- * ONE PROVIDER FOR THE STORE'S LANGUAGE.
13
- *
14
- * import { es } from "@cartbase/storefront/locales/es"
15
- * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
16
- *
17
- * Every CLIENT component inside it speaks the pack, and nothing else has to
18
- * be wired: it mounts the three label contexts (products, checkout, order),
19
- * and the cart drawer and the reviews family read the language from it
20
- * directly (`useLocaleArea`, since 2026-09-14). It does not MOUNT the cart
21
- * drawer, because that provider carries the cart's state, its client and its
22
- * persistence callbacks, which belong where the app owns that state; the
23
- * drawer only reads the language from here.
24
- *
25
- * What it cannot reach is a SERVER component, because a server component has
26
- * no context. The store, product and order templates render on the server,
27
- * so each page hands them their area of the pack as a REQUIRED prop:
28
- * `PropLabelAreas` in ./types names them, with the mounts written out.
29
- *
30
- * DEFAULT IS ENGLISH. A component rendered outside this provider reads the
31
- * library's own English defaults, so nothing renders blank or crashes for
32
- * want of a provider.
33
- */
34
- export function StorefrontLocaleProvider({
35
- locale,
36
- children,
37
- }: {
38
- locale: StorefrontLocale
39
- children: ReactNode
40
- }) {
41
- return (
42
- <StorefrontLocaleContext.Provider value={locale}>
43
- <ProductLabelsProvider labels={locale.products}>
44
- <CheckoutProvider labels={locale.checkout}>
45
- <OrderLabelsProvider labels={locale.order}>{children}</OrderLabelsProvider>
46
- </CheckoutProvider>
47
- </ProductLabelsProvider>
48
- </StorefrontLocaleContext.Provider>
49
- )
50
- }
51
-
52
- /**
53
- * The whole mounted locale, English when none is mounted: for the code that
54
- * needs the language itself, e.g. `<html lang={locale.code}>`, and for a
55
- * client shell that hands a server template its pack.
56
- */
57
- export function useStorefrontLocale(): StorefrontLocale {
58
- return useContext(StorefrontLocaleContext) ?? en
59
- }
1
+ "use client"
2
+
3
+ import { useContext, type ReactNode } from "react"
4
+ import { CheckoutProvider } from "../checkout/context"
5
+ import { OrderLabelsProvider } from "../order/context"
6
+ import { ProductLabelsProvider } from "../products/context"
7
+ import { StorefrontLocaleContext } from "./context"
8
+ import type { StorefrontLocale } from "./types"
9
+ import { en } from "./en"
10
+
11
+ /**
12
+ * ONE PROVIDER FOR THE STORE'S LANGUAGE.
13
+ *
14
+ * import { es } from "@cartbase/storefront/locales/es"
15
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
16
+ *
17
+ * Every CLIENT component inside it speaks the pack, and nothing else has to
18
+ * be wired: it mounts the three label contexts (products, checkout, order),
19
+ * and the cart drawer and the reviews family read the language from it
20
+ * directly (`useLocaleArea`, since 2026-09-14). It does not MOUNT the cart
21
+ * drawer, because that provider carries the cart's state, its client and its
22
+ * persistence callbacks, which belong where the app owns that state; the
23
+ * drawer only reads the language from here.
24
+ *
25
+ * What it cannot reach is a SERVER component, because a server component has
26
+ * no context. The store, product and order templates render on the server,
27
+ * so each page hands them their area of the pack as a REQUIRED prop:
28
+ * `PropLabelAreas` in ./types names them, with the mounts written out.
29
+ *
30
+ * DEFAULT IS ENGLISH. A component rendered outside this provider reads the
31
+ * library's own English defaults, so nothing renders blank or crashes for
32
+ * want of a provider.
33
+ */
34
+ export function StorefrontLocaleProvider({
35
+ locale,
36
+ children,
37
+ }: {
38
+ locale: StorefrontLocale
39
+ children: ReactNode
40
+ }) {
41
+ return (
42
+ <StorefrontLocaleContext.Provider value={locale}>
43
+ <ProductLabelsProvider labels={locale.products}>
44
+ <CheckoutProvider labels={locale.checkout}>
45
+ <OrderLabelsProvider labels={locale.order}>{children}</OrderLabelsProvider>
46
+ </CheckoutProvider>
47
+ </ProductLabelsProvider>
48
+ </StorefrontLocaleContext.Provider>
49
+ )
50
+ }
51
+
52
+ /**
53
+ * The whole mounted locale, English when none is mounted: for the code that
54
+ * needs the language itself, e.g. `<html lang={locale.code}>`, and for a
55
+ * client shell that hands a server template its pack.
56
+ */
57
+ export function useStorefrontLocale(): StorefrontLocale {
58
+ return useContext(StorefrontLocaleContext) ?? en
59
+ }