@usequeek/theme-kit 0.1.20 → 0.1.22

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 (94) hide show
  1. package/README.md +264 -77
  2. package/api/collections.ts +5 -5
  3. package/api/shop.ts +1 -1
  4. package/client.ts +2 -0
  5. package/components/auth/auth-email-login-step.tsx +9 -7
  6. package/components/auth/auth-email-otp-step.tsx +9 -7
  7. package/components/auth/auth-email-otp-verify-step.tsx +8 -6
  8. package/components/auth/auth-email-register-step.tsx +10 -8
  9. package/components/auth/auth-signup-step.tsx +8 -6
  10. package/components/blog/category-filter.tsx +6 -5
  11. package/components/blog/pagination.tsx +8 -7
  12. package/components/blog/post-meta.tsx +15 -8
  13. package/components/blog/related-posts.tsx +5 -3
  14. package/components/blog/share-buttons.tsx +10 -8
  15. package/components/checkout/checkout-contact.tsx +9 -7
  16. package/components/checkout/default-checkout-shell.tsx +17 -15
  17. package/components/checkout/policy-links.tsx +5 -3
  18. package/components/image.tsx +5 -5
  19. package/components/language-switcher.css +131 -0
  20. package/components/language-switcher.tsx +144 -0
  21. package/components/markdown.tsx +1 -1
  22. package/components/powered-by-queek.tsx +4 -4
  23. package/components/product-media-frame.tsx +1 -1
  24. package/components/product-metafields.tsx +2 -3
  25. package/components/variant-picker.tsx +6 -9
  26. package/demo.ts +10 -14
  27. package/design-token-preview.tsx +0 -2
  28. package/edit-mode-active.tsx +3 -4
  29. package/hooks/use-announcement.ts +1 -1
  30. package/hooks/use-auth-flow.tsx +19 -18
  31. package/hooks/use-locales.ts +107 -0
  32. package/hooks/use-posts.ts +1 -1
  33. package/hooks/use-product-detail.ts +1 -2
  34. package/hooks/use-product-sections.ts +4 -6
  35. package/hooks/use-products.ts +5 -8
  36. package/index.ts +16 -15
  37. package/locales/en.default.json +49 -7
  38. package/next-reference.d.ts +2 -4
  39. package/package.json +3 -2
  40. package/page-renderer.tsx +5 -6
  41. package/provider.tsx +22 -2
  42. package/scripts/verify-package.ts +6 -8
  43. package/sdk/client.ts +13 -12
  44. package/sdk/error-handler.ts +1 -1
  45. package/sdk/platform.ts +6 -8
  46. package/sdk/queek-client.ts +11 -12
  47. package/sdk/server-store-client.ts +13 -17
  48. package/shared-blocks/metaobjects.tsx +1 -1
  49. package/stores/cart-store.ts +2 -2
  50. package/stores/checkout-store.ts +1 -1
  51. package/stores/edit-preview-store.ts +5 -6
  52. package/stores/user-store.ts +4 -5
  53. package/strings/pseudo-locale.ts +263 -0
  54. package/strings/theme-strings.ts +185 -20
  55. package/tests/auth-email-errors.test.tsx +55 -0
  56. package/tests/currency-contract.test.ts +2 -3
  57. package/tests/display-format.test.ts +356 -2
  58. package/tests/fixtures/external-theme.tsx +18 -2
  59. package/tests/language-switcher.test.tsx +350 -0
  60. package/tests/list-pagination.test.tsx +8 -8
  61. package/tests/locale-switch.test.ts +197 -0
  62. package/tests/locale-transport.test.tsx +3 -3
  63. package/tests/markdown-render.test.ts +5 -6
  64. package/tests/orders-walk.test.ts +5 -5
  65. package/tests/page-renderer-signature.test.tsx +1 -1
  66. package/tests/powered-by-queek.test.tsx +3 -3
  67. package/tests/powered-by-url.test.ts +2 -2
  68. package/tests/public-surface.test.ts +1 -4
  69. package/tests/shopper-chrome-strings.test.tsx +271 -0
  70. package/tests/storefront-client-scope.test.ts +5 -5
  71. package/tests/theme-hook-placement.test.tsx +179 -0
  72. package/tests/theme-strings-client.test.tsx +5 -5
  73. package/tests/theme-strings-pseudo.test.tsx +171 -0
  74. package/tests/theme-strings-typed.test.tsx +97 -0
  75. package/tests/theme-strings.test.tsx +10 -7
  76. package/tests/theme-typed-keys.fixture.ts +76 -0
  77. package/tests/use-shop-prefetch.test.tsx +1 -1
  78. package/theme-mount.tsx +6 -11
  79. package/types/block.ts +7 -7
  80. package/types/media.ts +8 -9
  81. package/types/product.ts +8 -12
  82. package/types/theme.ts +26 -25
  83. package/types/vendor.ts +10 -12
  84. package/utils/announcement.ts +3 -3
  85. package/utils/benefit-icon.tsx +9 -11
  86. package/utils/brand.ts +7 -10
  87. package/utils/format.ts +100 -14
  88. package/utils/image-srcset.ts +2 -2
  89. package/utils/locale-switch.ts +208 -0
  90. package/utils/locale.ts +56 -5
  91. package/utils/menu-link.ts +1 -1
  92. package/utils/product-media.ts +1 -1
  93. package/utils/product-normalizer.ts +2 -2
  94. package/utils/vendor-host.ts +7 -11
package/utils/locale.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Storefront locale transport — the kit side of the translations plan.
2
+ * Storefront locale transport.
3
3
  *
4
- * Contract (translations-core.md, decisions closed): the backend accepts
5
- * `?locale=<code>` on every customer-facing read and answers translated text
6
- * (unknown/unpublished locale → source text, no error). The storefront proxy
7
- * sets `x-queek-locale` on the incoming request for published NON-primary
4
+ * The API accepts `?locale=<code>` on every customer-facing read and answers
5
+ * translated text (unknown/unpublished locale → source text, no error). The
6
+ * storefront proxy sets `x-queek-locale` on the incoming request for published NON-primary
8
7
  * locales only — the header is ABSENT on the primary locale — and suffixes
9
8
  * revalidation tags as `<tag>:locale:<code>`.
10
9
  *
@@ -19,6 +18,58 @@
19
18
  * byte-identical to before.
20
19
  */
21
20
 
21
+ /**
22
+ * English display locale — the ONE Intl tag every English shopper sees dates,
23
+ * numbers and money in. `en-NG`, not `en-GB`: both print dates identically
24
+ * ("2 October 2026", "14 Feb 2026"), but only `en-NG` renders NGN with the
25
+ * naira sign the stores already show (`₦12,500` vs `en-GB`'s `NGN 12,500`, on
26
+ * Node 22 with full ICU). `DISPLAY_LOCALE` in `utils/format.ts` aliases this, so
27
+ * the value lives in exactly one place.
28
+ */
29
+ export const ENGLISH_DISPLAY_LOCALE = 'en-NG';
30
+
31
+ /**
32
+ * THE single canonical mapping from a storefront locale code to the Intl
33
+ * locale dates/numbers/relative time are formatted in: the shopper's locale,
34
+ * with English in the British/Nigerian style ("2 October 2026", never US
35
+ * "October 2, 2026").
36
+ *
37
+ * - Missing/invalid/unknown-structure codes and EVERY English variant (`en`,
38
+ * `en-GB`, `en-NG`, `en-US`, …) become `ENGLISH_DISPLAY_LOCALE`. In
39
+ * particular bare `en` must never reach Intl directly: `en` formats US
40
+ * order ("October 2, 2026").
41
+ * - Every other well-formed code passes through unchanged (`fr`, `ar`,
42
+ * `pt-BR`, `zh-CN`, `yo`, `ha`, `sw`, …).
43
+ *
44
+ * Pure (no `next/*`, no React) like the rest of this module. Never throws.
45
+ * Pair with `resolveSupportedLocale` when the runtime's ICU may lack the
46
+ * locale — this function maps, it does not probe.
47
+ */
48
+ export function resolveIntlLocale(locale: string | null | undefined): string {
49
+ const code = parseLocaleCode(locale);
50
+ if (!code) return ENGLISH_DISPLAY_LOCALE;
51
+ if ((code.split('-')[0] ?? '').toLowerCase() === 'en') return ENGLISH_DISPLAY_LOCALE;
52
+ return code;
53
+ }
54
+
55
+ /**
56
+ * Graceful ICU fallback for one formatting call: the candidate when this
57
+ * Node's ICU formats it natively, `ENGLISH_DISPLAY_LOCALE` otherwise (a
58
+ * minimal-ICU build with no French data still renders English, never an
59
+ * exception). Never throws — pass any Intl constructor's
60
+ * `supportedLocalesOf`, e.g. `(l) => Intl.DateTimeFormat.supportedLocalesOf(l)`.
61
+ */
62
+ export function resolveSupportedLocale(
63
+ candidate: string,
64
+ supportedLocalesOf: (locales: string[]) => readonly string[],
65
+ ): string {
66
+ try {
67
+ return supportedLocalesOf([candidate]).length > 0 ? candidate : ENGLISH_DISPLAY_LOCALE;
68
+ } catch {
69
+ return ENGLISH_DISPLAY_LOCALE;
70
+ }
71
+ }
72
+
22
73
  /** Request header the proxy sets for published non-primary locales. */
23
74
  export const LOCALE_CODE_HEADER = 'x-queek-locale';
24
75
 
@@ -17,7 +17,7 @@ export function menuItemToHref(item: MenuItem, basePath: string = ''): string {
17
17
  const href = `${basePath}/${item.ref}`;
18
18
  const anchor = item.meta?.anchor;
19
19
  // href may already end in "/" (the home page's ref is normalized to
20
- // "/" — see MenuResource::normalizeHomeRefs) — never emit a doubled
20
+ // "/") — never emit a doubled
21
21
  // slash before the fragment.
22
22
  return anchor ? `${href.replace(/\/$/, '')}#${anchor}` : href;
23
23
  }
@@ -14,7 +14,7 @@ export interface MediaFrame {
14
14
 
15
15
  /**
16
16
  * Picks the largest tier a PDP hero should ever request. `view` is capped at
17
- * 1600px (queek_backend config/media.php) which covers every theme's hero box
17
+ * 1600px, which covers every theme's hero box
18
18
  * at DPR2 — the raw `original` is uncapped and unoptimized.
19
19
  */
20
20
  function heroUrl(variants: ImageVariants | null | undefined): string | null {
@@ -37,8 +37,8 @@ function toBool(value: unknown, fallback = false): boolean {
37
37
 
38
38
  /**
39
39
  * Same base/sale/compare-at shape as normalizePricing(), but a variant's own price lives on
40
- * FLAT fields (price/discount_price/compare_at_price/discount_amount — see
41
- * ProductResource::resolveShopVariants on the backend), never nested under a `pricing` key.
40
+ * FLAT fields (price/discount_price/compare_at_price/discount_amount), never nested
41
+ * under a `pricing` key.
42
42
  * Reading `variant.pricing?.sale_amount` (as this used to) is always undefined, so every
43
43
  * variant silently fell back to the base product's price — picking Denim vs Taupe never
44
44
  * changed what was shown, even though the backend had already computed the right number.
@@ -1,14 +1,11 @@
1
1
  /**
2
2
  * Vendor resolution from a hostname: which store is this request for?
3
3
  *
4
- * Lived at `app/middleware.ts` until core needed it — `lib/core/api/products.ts`
5
- * and `lib/core/sdk/server-store-client.ts` both import from here, which meant
6
- * core depended on the app. Nothing here is app-specific: it reads env and
7
- * resolves a slug. (The actual Next middleware is `proxy.ts`; the old filename
8
- * suggested otherwise and was never that.)
4
+ * Nothing here is app-specific: it reads env and resolves a slug, so the kit's
5
+ * server fetchers and a host app's proxy can both import it.
9
6
  */
10
7
 
11
- // Mirrors backend config/storefront.php `reserved_slugs`. Keep in sync.
8
+ // Mirrors the API's reserved store slugs. Keep in sync.
12
9
  // These are subdomains the storefront proxy must NOT treat as vendor slugs.
13
10
  export const RESERVED_VENDOR_SLUGS = new Set([
14
11
  // Live platform subdomains
@@ -24,11 +21,10 @@ export const RESERVED_VENDOR_SLUGS = new Set([
24
21
  'staff', 'team', 'queek', 'merchant', 'vendor', 'rider', 'customer',
25
22
  ]);
26
23
 
27
- // Every sluggable backend model (Vendor, Product, Category, Region, Service —
28
- // app/Models/*.php `getSlugOptions()`) is Spatie Sluggable output: lowercase
29
- // alphanumeric, optionally hyphen-separated, never a dot/underscore/uppercase.
30
- // Anything else cannot be a real slug of any kind, so reject it before any
31
- // backend round-trip instead of burning calls that all 404. This is what stops
24
+ // Every sluggable API model (Vendor, Product, Category, Region, Service)
25
+ // produces lowercase alphanumeric slugs, optionally hyphen-separated, never a
26
+ // dot/underscore/uppercase. Anything else cannot be a real slug of any kind, so
27
+ // reject it before any API round-trip instead of burning calls that all 404. This is what stops
32
28
  // scanners probing `.env`, `.ssh`, `docker-compose.yaml`, `*.php` etc —
33
29
  // RESERVED_VENDOR_SLUGS alone only covers known words, not the infinite space
34
30
  // of file-extension guesses.