@usequeek/theme-kit 0.1.17 → 0.1.18
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/README.md +41 -0
- package/components/cart/default-cart-shell.tsx +23 -21
- package/index.ts +3 -0
- package/locales/en.default.json +138 -0
- package/package.json +2 -1
- package/provider.tsx +58 -1
- package/strings/theme-strings.ts +356 -0
- package/tests/theme-strings-client.test.tsx +182 -0
- package/tests/theme-strings.test.tsx +396 -0
package/README.md
CHANGED
|
@@ -42,6 +42,47 @@ Theme CSS ships with the package: `@usequeek/theme-kit/shared-blocks/core-blocks
|
|
|
42
42
|
(framework-owned blocks), plus per-component CSS next to its component
|
|
43
43
|
(`components/variant-picker.css`, `apps/apps.css`).
|
|
44
44
|
|
|
45
|
+
## Theme strings
|
|
46
|
+
|
|
47
|
+
The kit's Shopify-shaped UI-string mechanism (`strings/theme-strings.ts`,
|
|
48
|
+
pure: no `next/*`, no React). Additive and backward-compatible — a theme or
|
|
49
|
+
provider that passes nothing renders exactly as today, in English.
|
|
50
|
+
|
|
51
|
+
- **Authoring contract.** `t(dictionaries, key, vars?, locale?)`: dot-path
|
|
52
|
+
lookup (`cart.title`), `{var}` interpolation (`{{`/`}}` escape to literal
|
|
53
|
+
braces; a missing var keeps its `{placeholder}`), CLDR plurals via
|
|
54
|
+
`Intl.PluralRules` — a key holding `{ one, two, few, many, other }`
|
|
55
|
+
(nested, or flat `key.one` … `key.other` suffix keys) selects by
|
|
56
|
+
`vars.count`, defaulting to `other`. Locale-aware `formatThemeNumber`,
|
|
57
|
+
`formatThemeDate` and `formatThemeMoney` replace hardcoded `en-GB`
|
|
58
|
+
formatting; an unknown locale falls back to English. Keys are dotted
|
|
59
|
+
lowercase `scope.thing.state`, max **40 chars**; values max **1000 chars**:
|
|
60
|
+
the backend overlay stores `<theme-slug>.<key>` in a varchar(64), so the
|
|
61
|
+
key budget leaves 23 chars for the slug plus the dot.
|
|
62
|
+
- **Two surfaces.** Server (provider-free): the host supplies per-locale
|
|
63
|
+
loaders and calls
|
|
64
|
+
`createThemeStrings({ locale, loaders })` — `{ override, theme, core }`,
|
|
65
|
+
each `(locale) => Promise<dict | null>` — usually a per-locale dynamic
|
|
66
|
+
`import()` with a try/catch returning null when the file is absent — then
|
|
67
|
+
passes the bound `t` via props/closure. A throwing loader simply drops
|
|
68
|
+
out of the chain.
|
|
69
|
+
Client: `<StorefrontProvider strings={…}>` (optional, no-op when absent)
|
|
70
|
+
plus `useThemeStrings()` returning the same bound `t`; works inside
|
|
71
|
+
`ThemeMount`. The kit ships `locales/en.default.json` (cart, checkout,
|
|
72
|
+
auth, blog) as the last-resort English default.
|
|
73
|
+
- **Fallback.** Ordered dictionaries (override → theme → core → English
|
|
74
|
+
default); first hit wins. The last fallback is the English default
|
|
75
|
+
*value* — never the raw key, never "translation missing". Absent
|
|
76
|
+
everywhere renders `""` (and warns once per key in development only —
|
|
77
|
+
an empty string would otherwise leak internal key names into the UI).
|
|
78
|
+
- **Per-locale loading.** `createThemeStrings` reads ONLY the active
|
|
79
|
+
locale's dictionaries, and the host passes ONLY that locale to the
|
|
80
|
+
provider — other `{lang}.json` files never enter the client bundle.
|
|
81
|
+
Only `locales/en.default.json` may be statically imported (by
|
|
82
|
+
`strings/theme-strings.ts` itself); every other locale arrives via the
|
|
83
|
+
host's dynamic `import()` per locale. `tests/theme-strings.test.tsx`
|
|
84
|
+
gates this: any other static locale import fails the suite.
|
|
85
|
+
|
|
45
86
|
## Consumers
|
|
46
87
|
|
|
47
88
|
- **The Queek storefront** (`usequeek/queek-storefront`) installs this package
|
|
@@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import type { CartShellProps } from '../../types/theme';
|
|
7
7
|
import { useHref } from '../../hooks/use-href';
|
|
8
|
+
import { useThemeStrings } from '../../provider';
|
|
8
9
|
import { useCheckoutStore } from '../../stores/checkout-store';
|
|
9
10
|
import { useRelatedProducts } from '../../hooks/use-related-products';
|
|
10
11
|
import { useCart } from '../../hooks/use-cart';
|
|
@@ -27,6 +28,7 @@ export function DefaultCartShell({
|
|
|
27
28
|
onRemove,
|
|
28
29
|
}: CartShellProps): JSX.Element {
|
|
29
30
|
const href = useHref();
|
|
31
|
+
const t = useThemeStrings();
|
|
30
32
|
const { addProduct } = useCart();
|
|
31
33
|
const vendorNote = useCheckoutStore((s) => s.vendorNote);
|
|
32
34
|
const setVendorNote = useCheckoutStore((s) => s.setVendorNote);
|
|
@@ -48,10 +50,10 @@ export function DefaultCartShell({
|
|
|
48
50
|
if (empty) {
|
|
49
51
|
return (
|
|
50
52
|
<div className="core-cart-page">
|
|
51
|
-
<h1 className="core-cart-page__title">
|
|
53
|
+
<h1 className="core-cart-page__title">{t('cart.title')}</h1>
|
|
52
54
|
<div className="core-cart-page__empty">
|
|
53
|
-
<p>
|
|
54
|
-
<Link href={href('/')} className="core-checkout__back-link">
|
|
55
|
+
<p>{t('cart.empty.message')}</p>
|
|
56
|
+
<Link href={href('/')} className="core-checkout__back-link">{t('cart.empty.browse')}</Link>
|
|
55
57
|
</div>
|
|
56
58
|
</div>
|
|
57
59
|
);
|
|
@@ -59,7 +61,7 @@ export function DefaultCartShell({
|
|
|
59
61
|
|
|
60
62
|
return (
|
|
61
63
|
<div className="core-cart-page">
|
|
62
|
-
<h1 className="core-cart-page__title">
|
|
64
|
+
<h1 className="core-cart-page__title">{t('cart.title')}</h1>
|
|
63
65
|
|
|
64
66
|
<div className="core-cart-page__grid">
|
|
65
67
|
<div className="core-cart-page__main">
|
|
@@ -74,19 +76,19 @@ export function DefaultCartShell({
|
|
|
74
76
|
)}
|
|
75
77
|
<div className="core-cart-page__item-info">
|
|
76
78
|
<span className="core-cart-page__item-title">{item.title}</span>
|
|
77
|
-
<span className="core-cart-page__item-price">{formatMoney(item.unit_price, currency)}
|
|
79
|
+
<span className="core-cart-page__item-price">{t('cart.item.each', { price: formatMoney(item.unit_price, currency) })}</span>
|
|
78
80
|
</div>
|
|
79
81
|
<div className="core-cart-page__item-qty">
|
|
80
|
-
<button type="button" onClick={() => onDecrease(item.id, item.shop_id)} aria-label={
|
|
82
|
+
<button type="button" onClick={() => onDecrease(item.id, item.shop_id)} aria-label={t('cart.item.decrease', { title: item.title })}>
|
|
81
83
|
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 6h8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
|
|
82
84
|
</button>
|
|
83
85
|
<span>{item.quantity}</span>
|
|
84
|
-
<button type="button" onClick={() => onIncrease(item.id, item.shop_id)} aria-label={
|
|
86
|
+
<button type="button" onClick={() => onIncrease(item.id, item.shop_id)} aria-label={t('cart.item.increase', { title: item.title })}>
|
|
85
87
|
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M6 2v8M2 6h8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
|
|
86
88
|
</button>
|
|
87
89
|
</div>
|
|
88
90
|
<span className="core-cart-page__item-total">{formatMoney(item.unit_price * item.quantity, currency)}</span>
|
|
89
|
-
<button type="button" className="core-cart-page__item-remove" onClick={() => onRemove(item.id, item.shop_id)} aria-label={
|
|
91
|
+
<button type="button" className="core-cart-page__item-remove" onClick={() => onRemove(item.id, item.shop_id)} aria-label={t('cart.item.remove', { title: item.title })}>
|
|
90
92
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
91
93
|
<path d="M4 6h12M8 6V4.5A1.5 1.5 0 019.5 3h1A1.5 1.5 0 0112 4.5V6m-6 0v9.5A1.5 1.5 0 007.5 17h5a1.5 1.5 0 001.5-1.5V6" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
92
94
|
</svg>
|
|
@@ -99,12 +101,12 @@ export function DefaultCartShell({
|
|
|
99
101
|
{related.length > 0 ? (
|
|
100
102
|
<section className="core-cart-page__related">
|
|
101
103
|
<div className="core-cart-page__related-head">
|
|
102
|
-
<h2>
|
|
104
|
+
<h2>{t('cart.related.title')}</h2>
|
|
103
105
|
<div className="core-cart-page__related-nav">
|
|
104
|
-
<button type="button" onClick={() => scrollRelated(-1)} aria-label=
|
|
106
|
+
<button type="button" onClick={() => scrollRelated(-1)} aria-label={t('cart.related.prev')}>
|
|
105
107
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 13L5 8l5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
106
108
|
</button>
|
|
107
|
-
<button type="button" onClick={() => scrollRelated(1)} aria-label=
|
|
109
|
+
<button type="button" onClick={() => scrollRelated(1)} aria-label={t('cart.related.next')}>
|
|
108
110
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 3l5 5-5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
109
111
|
</button>
|
|
110
112
|
</div>
|
|
@@ -125,13 +127,13 @@ export function DefaultCartShell({
|
|
|
125
127
|
)}
|
|
126
128
|
<div className="core-cart-page__related-info">
|
|
127
129
|
<span className="core-cart-page__related-title">{product.title}</span>
|
|
128
|
-
<span className="core-cart-page__related-price">{product.pricing.is_price_from ? '
|
|
130
|
+
<span className="core-cart-page__related-price">{product.pricing.is_price_from ? t('cart.related.from', { price: formatMoney(product.pricing.sale_amount, currency) }) : formatMoney(product.pricing.sale_amount, currency)}</span>
|
|
129
131
|
</div>
|
|
130
132
|
{productRequiresVariant(product) ? (
|
|
131
133
|
<Link
|
|
132
134
|
className="core-cart-page__related-add"
|
|
133
135
|
href={href(`/products/${product.slug}`)}
|
|
134
|
-
aria-label={
|
|
136
|
+
aria-label={t('cart.related.choose', { title: product.title })}
|
|
135
137
|
>
|
|
136
138
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
137
139
|
<path d="M7 4l6 6-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
@@ -141,7 +143,7 @@ export function DefaultCartShell({
|
|
|
141
143
|
<button
|
|
142
144
|
type="button"
|
|
143
145
|
className="core-cart-page__related-add"
|
|
144
|
-
aria-label={
|
|
146
|
+
aria-label={t('cart.related.add', { title: product.title })}
|
|
145
147
|
onClick={() => addProduct(product)}
|
|
146
148
|
>
|
|
147
149
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
@@ -160,10 +162,10 @@ export function DefaultCartShell({
|
|
|
160
162
|
|
|
161
163
|
<div className="core-cart-page__summary">
|
|
162
164
|
<div className="core-cart-page__summary-card">
|
|
163
|
-
<h2 className="core-cart-page__summary-title">
|
|
165
|
+
<h2 className="core-cart-page__summary-title">{t('cart.summary.title')}</h2>
|
|
164
166
|
|
|
165
167
|
<button type="button" className="core-cart-page__note-toggle" onClick={() => setNoteOpen((v) => !v)}>
|
|
166
|
-
|
|
168
|
+
{t('cart.note.toggle')}
|
|
167
169
|
<span aria-hidden="true">{noteOpen ? '−' : '+'}</span>
|
|
168
170
|
</button>
|
|
169
171
|
{noteOpen ? (
|
|
@@ -172,18 +174,18 @@ export function DefaultCartShell({
|
|
|
172
174
|
rows={3}
|
|
173
175
|
value={vendorNote}
|
|
174
176
|
onChange={(event) => setVendorNote(event.target.value)}
|
|
175
|
-
placeholder=
|
|
177
|
+
placeholder={t('cart.note.placeholder')}
|
|
176
178
|
/>
|
|
177
179
|
) : null}
|
|
178
180
|
|
|
179
181
|
<div className="core-cart-page__subtotal">
|
|
180
|
-
<span>
|
|
182
|
+
<span>{t('cart.summary.subtotal')}</span>
|
|
181
183
|
<strong>{formatMoney(total, currency)} {currency}</strong>
|
|
182
184
|
</div>
|
|
183
185
|
|
|
184
186
|
<label className="core-cart-page__terms">
|
|
185
187
|
<input type="checkbox" checked={agreed} onChange={(event) => setAgreed(event.target.checked)} />
|
|
186
|
-
<span>
|
|
188
|
+
<span>{t('cart.terms.agree')}<Link href={href('/policies')}>{t('cart.terms.policy')}</Link></span>
|
|
187
189
|
</label>
|
|
188
190
|
|
|
189
191
|
<Link
|
|
@@ -197,9 +199,9 @@ export function DefaultCartShell({
|
|
|
197
199
|
<circle cx="8" cy="17" r="1" fill="currentColor" />
|
|
198
200
|
<circle cx="14" cy="17" r="1" fill="currentColor" />
|
|
199
201
|
</svg>
|
|
200
|
-
|
|
202
|
+
{t('cart.checkout.action')}
|
|
201
203
|
</Link>
|
|
202
|
-
<Link href={href('/')} className="core-cart-page__continue-link">
|
|
204
|
+
<Link href={href('/')} className="core-cart-page__continue-link">{t('cart.summary.continue')}</Link>
|
|
203
205
|
</div>
|
|
204
206
|
</div>
|
|
205
207
|
</div>
|
package/index.ts
CHANGED
|
@@ -49,5 +49,8 @@ export * from './utils/video-embed';
|
|
|
49
49
|
export * from './utils/match-variant';
|
|
50
50
|
export * from './utils/announcement';
|
|
51
51
|
|
|
52
|
+
// Theme strings — the pure/server-safe i18n core (no next/*, no React).
|
|
53
|
+
export * from './strings/theme-strings';
|
|
54
|
+
|
|
52
55
|
// Rendering a theme's own demo.json — what makes local preview possible.
|
|
53
56
|
export * from './demo';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cart": {
|
|
3
|
+
"title": "Your cart",
|
|
4
|
+
"empty": {
|
|
5
|
+
"message": "Your cart is empty.",
|
|
6
|
+
"browse": "Browse products"
|
|
7
|
+
},
|
|
8
|
+
"item": {
|
|
9
|
+
"each": "{price} each",
|
|
10
|
+
"decrease": "Decrease {title}",
|
|
11
|
+
"increase": "Increase {title}",
|
|
12
|
+
"remove": "Remove {title}"
|
|
13
|
+
},
|
|
14
|
+
"related": {
|
|
15
|
+
"title": "You may also like",
|
|
16
|
+
"prev": "Previous",
|
|
17
|
+
"next": "Next",
|
|
18
|
+
"choose": "Choose options for {title}",
|
|
19
|
+
"add": "Add {title} to cart",
|
|
20
|
+
"from": "From {price}"
|
|
21
|
+
},
|
|
22
|
+
"summary": {
|
|
23
|
+
"title": "Order summary",
|
|
24
|
+
"subtotal": "Subtotal:",
|
|
25
|
+
"continue": "Continue shopping"
|
|
26
|
+
},
|
|
27
|
+
"note": {
|
|
28
|
+
"toggle": "Add order note",
|
|
29
|
+
"placeholder": "Order instructions (optional)"
|
|
30
|
+
},
|
|
31
|
+
"terms": {
|
|
32
|
+
"agree": "I agree to the ",
|
|
33
|
+
"policy": "terms and refund policy"
|
|
34
|
+
},
|
|
35
|
+
"checkout": {
|
|
36
|
+
"action": "Checkout"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"checkout": {
|
|
40
|
+
"empty": {
|
|
41
|
+
"message": "Your cart is empty.",
|
|
42
|
+
"browse": "Browse products"
|
|
43
|
+
},
|
|
44
|
+
"page": {
|
|
45
|
+
"back": "Continue shopping"
|
|
46
|
+
},
|
|
47
|
+
"contact": {
|
|
48
|
+
"label": "Contact",
|
|
49
|
+
"signin": "Sign in",
|
|
50
|
+
"signout": "Sign out",
|
|
51
|
+
"options": "Account options"
|
|
52
|
+
},
|
|
53
|
+
"delivery": {
|
|
54
|
+
"title": "How you'll get it"
|
|
55
|
+
},
|
|
56
|
+
"zone": {
|
|
57
|
+
"title": "Shipping zone"
|
|
58
|
+
},
|
|
59
|
+
"address": {
|
|
60
|
+
"title": "Delivery address"
|
|
61
|
+
},
|
|
62
|
+
"schedule": {
|
|
63
|
+
"title": "Schedule"
|
|
64
|
+
},
|
|
65
|
+
"payment": {
|
|
66
|
+
"title": "Payment",
|
|
67
|
+
"secure": "All transactions are secure and encrypted."
|
|
68
|
+
},
|
|
69
|
+
"summary": {
|
|
70
|
+
"title": "Order summary",
|
|
71
|
+
"empty": "Your cart is empty",
|
|
72
|
+
"subtotal": "Subtotal",
|
|
73
|
+
"proceed": "Proceed to checkout"
|
|
74
|
+
},
|
|
75
|
+
"tabs": {
|
|
76
|
+
"summary": "Summary",
|
|
77
|
+
"checkout": "Checkout"
|
|
78
|
+
},
|
|
79
|
+
"policy": {
|
|
80
|
+
"close": "Close",
|
|
81
|
+
"view": "View full page",
|
|
82
|
+
"list": "Store policies"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"auth": {
|
|
86
|
+
"login": {
|
|
87
|
+
"title": "Sign in",
|
|
88
|
+
"subtitle": "Enter your email and password",
|
|
89
|
+
"email": "Email address",
|
|
90
|
+
"password": "Password"
|
|
91
|
+
},
|
|
92
|
+
"email": {
|
|
93
|
+
"title": "Sign in",
|
|
94
|
+
"subtitle": "Enter your email to continue",
|
|
95
|
+
"or": "or",
|
|
96
|
+
"placeholder": "your@email.com"
|
|
97
|
+
},
|
|
98
|
+
"code": {
|
|
99
|
+
"title": "Enter code",
|
|
100
|
+
"sent": "{length}-digit code sent to {email}",
|
|
101
|
+
"digit": "Digit {index}"
|
|
102
|
+
},
|
|
103
|
+
"register": {
|
|
104
|
+
"title": "Create account",
|
|
105
|
+
"subtitle": "Fill in your details to get started",
|
|
106
|
+
"first": "First name",
|
|
107
|
+
"last": "Last name (optional)",
|
|
108
|
+
"email": "Email address",
|
|
109
|
+
"password": "Password (min 8 characters)"
|
|
110
|
+
},
|
|
111
|
+
"signup": {
|
|
112
|
+
"title": "Create your account",
|
|
113
|
+
"subtitle": "Just a few details to get you started",
|
|
114
|
+
"first": "First name",
|
|
115
|
+
"last": "Last name (optional)"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"blog": {
|
|
119
|
+
"pagination": {
|
|
120
|
+
"prev": "← Previous",
|
|
121
|
+
"next": "Next →",
|
|
122
|
+
"nav": "Blog pagination",
|
|
123
|
+
"page": "Page {current}"
|
|
124
|
+
},
|
|
125
|
+
"filter": {
|
|
126
|
+
"all": "All",
|
|
127
|
+
"nav": "Blog categories"
|
|
128
|
+
},
|
|
129
|
+
"share": {
|
|
130
|
+
"label": "Share",
|
|
131
|
+
"action": "Share",
|
|
132
|
+
"x": "Share on X",
|
|
133
|
+
"facebook": "Share on Facebook",
|
|
134
|
+
"whatsapp": "Share on WhatsApp",
|
|
135
|
+
"copy": "Copy link"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usequeek/theme-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The framework a Queek storefront theme is built against.",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"**/*.ts",
|
|
24
24
|
"**/*.tsx",
|
|
25
25
|
"**/*.css",
|
|
26
|
+
"locales/*.json",
|
|
26
27
|
"README.md",
|
|
27
28
|
"LICENSE"
|
|
28
29
|
],
|
package/provider.tsx
CHANGED
|
@@ -9,6 +9,13 @@ import type { Product } from './types/product';
|
|
|
9
9
|
import type { StorefrontConfig, VendorProfile } from './types/vendor';
|
|
10
10
|
import { getBrandCssVariables } from './utils/brand';
|
|
11
11
|
import { parseLocaleCode } from './utils/locale';
|
|
12
|
+
import {
|
|
13
|
+
defaultThemeStrings,
|
|
14
|
+
t as translateThemeStrings,
|
|
15
|
+
type ThemeStringsDictionary,
|
|
16
|
+
type ThemeStringsFn,
|
|
17
|
+
type ThemeStringsVars,
|
|
18
|
+
} from './strings/theme-strings';
|
|
12
19
|
import { DesignTokenPreviewListener } from './design-token-preview';
|
|
13
20
|
|
|
14
21
|
export interface StorefrontPreviewData {
|
|
@@ -40,6 +47,9 @@ interface StorefrontContextValue {
|
|
|
40
47
|
/** Validated request locale (null = primary). Browser hooks read it via
|
|
41
48
|
* `useStorefrontLocale()` and send `?locale=` when it is set. */
|
|
42
49
|
locale: string | null;
|
|
50
|
+
/** Active locale's dictionaries (override, theme, core — the host merges and
|
|
51
|
+
* passes ONLY the active locale). Empty = English defaults only. */
|
|
52
|
+
strings: ThemeStringsDictionary[];
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export const StorefrontContext = createContext<StorefrontContextValue | null>(null);
|
|
@@ -52,6 +62,7 @@ export function StorefrontProvider({
|
|
|
52
62
|
basePath,
|
|
53
63
|
pagesChrome,
|
|
54
64
|
locale,
|
|
65
|
+
strings,
|
|
55
66
|
children,
|
|
56
67
|
}: {
|
|
57
68
|
vendor: VendorProfile;
|
|
@@ -67,10 +78,27 @@ export function StorefrontProvider({
|
|
|
67
78
|
* the host owns it, like `basePath`.
|
|
68
79
|
*/
|
|
69
80
|
locale?: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Active locale's theme-string dictionaries (merchant override, theme
|
|
83
|
+
* locale, kit core locale — the host merges and passes ONLY the active
|
|
84
|
+
* locale, loaded per-locale so no other language ships to the client).
|
|
85
|
+
* Optional — absent means English defaults, and every kit component
|
|
86
|
+
* renders exactly as before. Existing props are untouched.
|
|
87
|
+
*/
|
|
88
|
+
strings?: ThemeStringsDictionary | ThemeStringsDictionary[] | null;
|
|
70
89
|
children: ReactNode;
|
|
71
90
|
}): JSX.Element {
|
|
72
91
|
const resolvedBasePath = basePath ?? `/${vendor.slug ?? ''}`;
|
|
73
92
|
const resolvedLocale = parseLocaleCode(locale);
|
|
93
|
+
const resolvedStrings = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
!strings
|
|
96
|
+
? []
|
|
97
|
+
: (Array.isArray(strings) ? strings : [strings]).filter(
|
|
98
|
+
(d): d is ThemeStringsDictionary => !!d,
|
|
99
|
+
),
|
|
100
|
+
[strings],
|
|
101
|
+
);
|
|
74
102
|
const value = useMemo(
|
|
75
103
|
() => ({
|
|
76
104
|
vendor,
|
|
@@ -80,8 +108,9 @@ export function StorefrontProvider({
|
|
|
80
108
|
basePath: resolvedBasePath,
|
|
81
109
|
pagesChrome: pagesChrome ?? {},
|
|
82
110
|
locale: resolvedLocale,
|
|
111
|
+
strings: resolvedStrings,
|
|
83
112
|
}),
|
|
84
|
-
[config, menus, previewData, vendor, resolvedBasePath, pagesChrome, resolvedLocale],
|
|
113
|
+
[config, menus, previewData, vendor, resolvedBasePath, pagesChrome, resolvedLocale, resolvedStrings],
|
|
85
114
|
);
|
|
86
115
|
const brandRootRef = useRef<HTMLDivElement>(null);
|
|
87
116
|
|
|
@@ -120,3 +149,31 @@ export function useStorefrontLocale(): string | null {
|
|
|
120
149
|
|
|
121
150
|
return context.locale ?? null;
|
|
122
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The client theme-string surface: the bound `t` for the active locale
|
|
155
|
+
* (`strings` prop, then the English default — never the raw key). Works in
|
|
156
|
+
* themes mounted by `ThemeMount` (which renders inside the host's provider).
|
|
157
|
+
* No `strings` prop → the English defaults, so kit components render exactly
|
|
158
|
+
* as before and third-party themes keep working untranslated.
|
|
159
|
+
*/
|
|
160
|
+
export function useThemeStrings(): ThemeStringsFn {
|
|
161
|
+
const context = useContext(StorefrontContext);
|
|
162
|
+
|
|
163
|
+
if (!context) {
|
|
164
|
+
throw new Error('useThemeStrings must be used within StorefrontProvider');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { strings, locale } = context;
|
|
168
|
+
return useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
(key: string, vars?: ThemeStringsVars, overrideLocale?: string | null) =>
|
|
171
|
+
translateThemeStrings(
|
|
172
|
+
[...strings, defaultThemeStrings],
|
|
173
|
+
key,
|
|
174
|
+
vars,
|
|
175
|
+
overrideLocale ?? locale ?? undefined,
|
|
176
|
+
),
|
|
177
|
+
[strings, locale],
|
|
178
|
+
);
|
|
179
|
+
}
|