@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.
- package/README.md +264 -77
- package/api/collections.ts +5 -5
- package/api/shop.ts +1 -1
- package/client.ts +2 -0
- package/components/auth/auth-email-login-step.tsx +9 -7
- package/components/auth/auth-email-otp-step.tsx +9 -7
- package/components/auth/auth-email-otp-verify-step.tsx +8 -6
- package/components/auth/auth-email-register-step.tsx +10 -8
- package/components/auth/auth-signup-step.tsx +8 -6
- package/components/blog/category-filter.tsx +6 -5
- package/components/blog/pagination.tsx +8 -7
- package/components/blog/post-meta.tsx +15 -8
- package/components/blog/related-posts.tsx +5 -3
- package/components/blog/share-buttons.tsx +10 -8
- package/components/checkout/checkout-contact.tsx +9 -7
- package/components/checkout/default-checkout-shell.tsx +17 -15
- package/components/checkout/policy-links.tsx +5 -3
- package/components/image.tsx +5 -5
- package/components/language-switcher.css +131 -0
- package/components/language-switcher.tsx +144 -0
- package/components/markdown.tsx +1 -1
- package/components/powered-by-queek.tsx +4 -4
- package/components/product-media-frame.tsx +1 -1
- package/components/product-metafields.tsx +2 -3
- package/components/variant-picker.tsx +6 -9
- package/demo.ts +10 -14
- package/design-token-preview.tsx +0 -2
- package/edit-mode-active.tsx +3 -4
- package/hooks/use-announcement.ts +1 -1
- package/hooks/use-auth-flow.tsx +19 -18
- package/hooks/use-locales.ts +107 -0
- package/hooks/use-posts.ts +1 -1
- package/hooks/use-product-detail.ts +1 -2
- package/hooks/use-product-sections.ts +4 -6
- package/hooks/use-products.ts +5 -8
- package/index.ts +16 -15
- package/locales/en.default.json +49 -7
- package/next-reference.d.ts +2 -4
- package/package.json +3 -2
- package/page-renderer.tsx +5 -6
- package/provider.tsx +22 -2
- package/scripts/verify-package.ts +6 -8
- package/sdk/client.ts +13 -12
- package/sdk/error-handler.ts +1 -1
- package/sdk/platform.ts +6 -8
- package/sdk/queek-client.ts +11 -12
- package/sdk/server-store-client.ts +13 -17
- package/shared-blocks/metaobjects.tsx +1 -1
- package/stores/cart-store.ts +2 -2
- package/stores/checkout-store.ts +1 -1
- package/stores/edit-preview-store.ts +5 -6
- package/stores/user-store.ts +4 -5
- package/strings/pseudo-locale.ts +263 -0
- package/strings/theme-strings.ts +185 -20
- package/tests/auth-email-errors.test.tsx +55 -0
- package/tests/currency-contract.test.ts +2 -3
- package/tests/display-format.test.ts +356 -2
- package/tests/fixtures/external-theme.tsx +18 -2
- package/tests/language-switcher.test.tsx +350 -0
- package/tests/list-pagination.test.tsx +8 -8
- package/tests/locale-switch.test.ts +197 -0
- package/tests/locale-transport.test.tsx +3 -3
- package/tests/markdown-render.test.ts +5 -6
- package/tests/orders-walk.test.ts +5 -5
- package/tests/page-renderer-signature.test.tsx +1 -1
- package/tests/powered-by-queek.test.tsx +3 -3
- package/tests/powered-by-url.test.ts +2 -2
- package/tests/public-surface.test.ts +1 -4
- package/tests/shopper-chrome-strings.test.tsx +271 -0
- package/tests/storefront-client-scope.test.ts +5 -5
- package/tests/theme-hook-placement.test.tsx +179 -0
- package/tests/theme-strings-client.test.tsx +5 -5
- package/tests/theme-strings-pseudo.test.tsx +171 -0
- package/tests/theme-strings-typed.test.tsx +97 -0
- package/tests/theme-strings.test.tsx +10 -7
- package/tests/theme-typed-keys.fixture.ts +76 -0
- package/tests/use-shop-prefetch.test.tsx +1 -1
- package/theme-mount.tsx +6 -11
- package/types/block.ts +7 -7
- package/types/media.ts +8 -9
- package/types/product.ts +8 -12
- package/types/theme.ts +26 -25
- package/types/vendor.ts +10 -12
- package/utils/announcement.ts +3 -3
- package/utils/benefit-icon.tsx +9 -11
- package/utils/brand.ts +7 -10
- package/utils/format.ts +100 -14
- package/utils/image-srcset.ts +2 -2
- package/utils/locale-switch.ts +208 -0
- package/utils/locale.ts +56 -5
- package/utils/menu-link.ts +1 -1
- package/utils/product-media.ts +1 -1
- package/utils/product-normalizer.ts +2 -2
- package/utils/vendor-host.ts +7 -11
package/strings/theme-strings.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Theme strings — the kit's
|
|
2
|
+
* Theme strings — the kit's UI-string mechanism: dotted keys, `{var}`
|
|
3
|
+
* interpolation, CLDR plurals and per-locale dictionaries.
|
|
3
4
|
*
|
|
4
5
|
* PURE AND SERVER-SAFE: this module imports nothing from `next/*` and nothing
|
|
5
6
|
* from React, so server components, Node tooling and the browser all share it
|
|
6
7
|
* (precedent: `utils/locale.ts`). Keep it that way — `tests/theme-strings*`
|
|
7
8
|
* fails a build that adds either import here.
|
|
8
9
|
*
|
|
9
|
-
* Two surfaces
|
|
10
|
+
* Two surfaces:
|
|
10
11
|
* - Server: `createThemeStrings({ locale, loaders })` returns a bound `t`
|
|
11
12
|
* that a server component receives via props/closure. NO provider in the
|
|
12
13
|
* tree. The host supplies the loaders (per-locale dynamic imports) so the
|
|
@@ -15,18 +16,24 @@
|
|
|
15
16
|
* `useThemeStrings()` returns the same bound `t`. Absent prop = English
|
|
16
17
|
* defaults, byte-identical to the old hardcoded literals.
|
|
17
18
|
*
|
|
18
|
-
* Fallback
|
|
19
|
+
* Fallback: the caller supplies an ordered list of
|
|
19
20
|
* dictionaries (merchant override, theme locale, kit core locale) and `t`
|
|
20
21
|
* returns the FIRST hit. The LAST entry is always the English default value —
|
|
21
22
|
* never the raw dotted key, never "translation missing". A key absent
|
|
22
23
|
* everywhere returns `''` (and warns once per key in development only).
|
|
23
24
|
*
|
|
24
25
|
* Key budget: dotted lowercase `scope.thing.state`, total length <= 40 chars.
|
|
25
|
-
*
|
|
26
|
-
* the key leaves 23 for the slug plus the dot separator.
|
|
26
|
+
* Merchant overrides are stored as `<theme-slug>.<key>` in a 64-character
|
|
27
|
+
* field, so 40 for the key leaves 23 for the slug plus the dot separator.
|
|
28
|
+
* Values <= 1000 chars.
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
ENGLISH_DISPLAY_LOCALE,
|
|
33
|
+
parseLocaleCode,
|
|
34
|
+
resolveIntlLocale,
|
|
35
|
+
resolveSupportedLocale,
|
|
36
|
+
} from '../utils/locale';
|
|
30
37
|
import enDefault from '../locales/en.default.json';
|
|
31
38
|
|
|
32
39
|
/** Interpolation variables: `{name}` in a value. `count` drives plurals. */
|
|
@@ -73,10 +80,19 @@ export const THEME_STRING_VALUE_MAX_LENGTH = 1000;
|
|
|
73
80
|
const PLURAL_FORMS = ['one', 'two', 'few', 'many', 'other'] as const;
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
|
-
* Canonical locale for
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
83
|
+
* Canonical locale for PLURAL SELECTION ONLY (used by `t()`).
|
|
84
|
+
*
|
|
85
|
+
* This returns a bare `'en'` tag for unknown codes, and bare `'en'` is a
|
|
86
|
+
* US-order Intl tag (`February 15, 2026`). NEVER pass its result to
|
|
87
|
+
* `Intl.DateTimeFormat` / `Intl.NumberFormat` / `Intl.RelativeTimeFormat`,
|
|
88
|
+
* `toLocale*`, or any date/number/money/relative-time formatter — that would
|
|
89
|
+
* reintroduce US date order (`February 15, 2026`) for English stores.
|
|
90
|
+
* Formatters must route through `resolveIntlLocale` (en* → en-NG) instead.
|
|
91
|
+
*
|
|
92
|
+
* Unknown/unparseable codes become English, mirroring `parseLocaleCode` (and
|
|
93
|
+
* the API's unknown-locale rule). A well-formed code the runtime's ICU
|
|
94
|
+
* does not know also becomes English, so plural selection never silently
|
|
95
|
+
* follows the wrong language.
|
|
80
96
|
*/
|
|
81
97
|
export function canonicalThemeLocale(locale: string | null | undefined): string {
|
|
82
98
|
const code = parseLocaleCode(locale) ?? 'en';
|
|
@@ -315,16 +331,48 @@ export async function createThemeStrings(options?: {
|
|
|
315
331
|
};
|
|
316
332
|
}
|
|
317
333
|
|
|
318
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Pick the Intl tag for one theme formatting call: the storefront locale
|
|
336
|
+
* through the single canonical mapping (`resolveIntlLocale` — English lands
|
|
337
|
+
* on `ENGLISH_DISPLAY_LOCALE`, never bare `en` with its US order), or
|
|
338
|
+
* English when the runtime's ICU lacks the locale. Never throws.
|
|
339
|
+
*/
|
|
340
|
+
function themeFormatTag(
|
|
341
|
+
locale: string | null | undefined,
|
|
342
|
+
supportedLocalesOf: (locales: string[]) => readonly string[],
|
|
343
|
+
): string {
|
|
344
|
+
return resolveSupportedLocale(resolveIntlLocale(locale), supportedLocalesOf);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Locale-aware number formatting keyed by the active locale (unknown → English). Never throws. */
|
|
319
348
|
export function formatThemeNumber(
|
|
320
349
|
value: number,
|
|
321
350
|
locale?: string | null,
|
|
322
351
|
options?: Intl.NumberFormatOptions,
|
|
323
352
|
): string {
|
|
324
|
-
|
|
353
|
+
const tag = themeFormatTag(locale, (l) => Intl.NumberFormat.supportedLocalesOf(l));
|
|
354
|
+
try {
|
|
355
|
+
return new Intl.NumberFormat(tag, options).format(value);
|
|
356
|
+
} catch {
|
|
357
|
+
try {
|
|
358
|
+
return new Intl.NumberFormat(ENGLISH_DISPLAY_LOCALE, options).format(value);
|
|
359
|
+
} catch {
|
|
360
|
+
return String(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
325
363
|
}
|
|
326
364
|
|
|
327
|
-
/**
|
|
365
|
+
/**
|
|
366
|
+
* Locale-aware date formatting keyed by the active locale (unknown →
|
|
367
|
+
* English). `''` if unparseable. Never throws.
|
|
368
|
+
*
|
|
369
|
+
* THE helper themes call instead of hardcoding 'en-GB' in their own date
|
|
370
|
+
* formatting: pass the storefront locale (client: `useStorefrontLocale()`; server: the
|
|
371
|
+
* request locale the host already holds) and the same Intl options — English
|
|
372
|
+
* shoppers see the unchanged British/Nigerian order ("2 October 2026"),
|
|
373
|
+
* other shoppers see their own locale. A Node whose ICU lacks the locale
|
|
374
|
+
* falls back to English, never an exception.
|
|
375
|
+
*/
|
|
328
376
|
export function formatThemeDate(
|
|
329
377
|
value: string | number | Date,
|
|
330
378
|
locale?: string | null,
|
|
@@ -332,7 +380,16 @@ export function formatThemeDate(
|
|
|
332
380
|
): string {
|
|
333
381
|
const date = value instanceof Date ? value : new Date(value);
|
|
334
382
|
if (Number.isNaN(date.getTime())) return '';
|
|
335
|
-
|
|
383
|
+
const tag = themeFormatTag(locale, (l) => Intl.DateTimeFormat.supportedLocalesOf(l));
|
|
384
|
+
try {
|
|
385
|
+
return new Intl.DateTimeFormat(tag, options).format(date);
|
|
386
|
+
} catch {
|
|
387
|
+
try {
|
|
388
|
+
return new Intl.DateTimeFormat(ENGLISH_DISPLAY_LOCALE, options).format(date);
|
|
389
|
+
} catch {
|
|
390
|
+
return '';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
function normalizeMoneyCurrency(value: unknown): string {
|
|
@@ -341,16 +398,124 @@ function normalizeMoneyCurrency(value: unknown): string {
|
|
|
341
398
|
: 'NGN';
|
|
342
399
|
}
|
|
343
400
|
|
|
344
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* OPTIONAL typed keys (additive — the untyped `t(key: string, …)` above is
|
|
403
|
+
* untouched, so every existing caller keeps compiling).
|
|
404
|
+
*
|
|
405
|
+
* `ThemeStringKey<typeof strings>` derives the key union from a theme's
|
|
406
|
+
* English dictionary object: nested objects flatten to dotted keys
|
|
407
|
+
* (`{ cart: { title: '…' } }` → `'cart.title'`), a plural map
|
|
408
|
+
* (`{ one, …, other }`) counts as ONE key (`'items.count'`, never
|
|
409
|
+
* `'items.count.one'`), and a flat literal dotted key (`'a.b.c'`) stays one
|
|
410
|
+
* key. A broad `ThemeStringsDictionary` (index signature) collapses to
|
|
411
|
+
* `string` — i.e. untyped — by design.
|
|
412
|
+
*
|
|
413
|
+
* Pair with `createThemeT` for a drop-in typed binding:
|
|
414
|
+
*
|
|
415
|
+
* ```ts
|
|
416
|
+
* import enDefault from './locales/en.default.json';
|
|
417
|
+
* const { t } = await createThemeStrings({ locale });
|
|
418
|
+
* const tt = createThemeT<typeof enDefault>(t);
|
|
419
|
+
* tt('cart.title'); // ok
|
|
420
|
+
* // tt('cart.titl'); // type error: typo key fails
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
export type ThemePluralForm = 'one' | 'two' | 'few' | 'many' | 'other';
|
|
424
|
+
|
|
425
|
+
type IsThemePluralMap<V> = V extends { other: string }
|
|
426
|
+
? Exclude<keyof V, ThemePluralForm> extends never
|
|
427
|
+
? true
|
|
428
|
+
: false
|
|
429
|
+
: false;
|
|
430
|
+
|
|
431
|
+
export type ThemeStringKey<T, Prefix extends string = ''> = string extends Extract<
|
|
432
|
+
keyof T,
|
|
433
|
+
string
|
|
434
|
+
>
|
|
435
|
+
? string
|
|
436
|
+
: {
|
|
437
|
+
[K in Extract<keyof T, string>]: T[K] extends string
|
|
438
|
+
? `${Prefix}${K}`
|
|
439
|
+
: IsThemePluralMap<T[K]> extends true
|
|
440
|
+
? `${Prefix}${K}`
|
|
441
|
+
: T[K] extends Record<string, unknown>
|
|
442
|
+
? ThemeStringKey<T[K], `${Prefix}${K}.`>
|
|
443
|
+
: `${Prefix}${K}`;
|
|
444
|
+
}[Extract<keyof T, string>];
|
|
445
|
+
|
|
446
|
+
/** The typed binding `createThemeT` hands out: same call shape as `t`. */
|
|
447
|
+
export type TypedThemeStringsFn<TKey extends string = string> = (
|
|
448
|
+
key: TKey,
|
|
449
|
+
vars?: ThemeStringsVars,
|
|
450
|
+
locale?: string | null,
|
|
451
|
+
) => string;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Wrap any bound `t` (server `createThemeStrings` or client
|
|
455
|
+
* `useThemeStrings()`) so only keys of `TDictionary` compile. Pass the
|
|
456
|
+
* theme's English dictionary type explicitly:
|
|
457
|
+
* `createThemeT<typeof enDefault>(t)`. Pass a broad
|
|
458
|
+
* `ThemeStringsDictionary` and the result stays `(key: string, …)` — the
|
|
459
|
+
* untyped behaviour, unchanged.
|
|
460
|
+
*/
|
|
461
|
+
export function createThemeT<TDictionary>(
|
|
462
|
+
translate: ThemeStringsFn,
|
|
463
|
+
): TypedThemeStringsFn<ThemeStringKey<TDictionary>> {
|
|
464
|
+
return translate as TypedThemeStringsFn<ThemeStringKey<TDictionary>>;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Locale-aware currency formatting keyed by the active locale (unknown → English). Never throws. */
|
|
345
468
|
export function formatThemeMoney(
|
|
346
469
|
amount: number,
|
|
347
470
|
currency: unknown,
|
|
348
471
|
locale?: string | null,
|
|
349
472
|
options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>,
|
|
350
473
|
): string {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
474
|
+
const code = normalizeMoneyCurrency(currency);
|
|
475
|
+
const tag = themeFormatTag(locale, (l) => Intl.NumberFormat.supportedLocalesOf(l));
|
|
476
|
+
try {
|
|
477
|
+
return new Intl.NumberFormat(tag, {
|
|
478
|
+
...options,
|
|
479
|
+
style: 'currency',
|
|
480
|
+
currency: code,
|
|
481
|
+
}).format(amount);
|
|
482
|
+
} catch {
|
|
483
|
+
try {
|
|
484
|
+
return new Intl.NumberFormat(ENGLISH_DISPLAY_LOCALE, {
|
|
485
|
+
...options,
|
|
486
|
+
style: 'currency',
|
|
487
|
+
currency: code,
|
|
488
|
+
}).format(amount);
|
|
489
|
+
} catch {
|
|
490
|
+
return `${code} ${amount}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Locale-aware relative time keyed by the active locale (unknown → English):
|
|
497
|
+
* `formatThemeRelativeTime(-1, 'day', 'fr')` → `"hier"`. `numeric: 'auto'`
|
|
498
|
+
* by default so whole days read as words where the language has them. Never
|
|
499
|
+
* throws — a missing ICU locale falls back to English, a bad unit to
|
|
500
|
+
* `"<value> <unit>"`.
|
|
501
|
+
*/
|
|
502
|
+
export function formatThemeRelativeTime(
|
|
503
|
+
value: number,
|
|
504
|
+
unit: Intl.RelativeTimeFormatUnit = 'day',
|
|
505
|
+
locale?: string | null,
|
|
506
|
+
options?: Intl.RelativeTimeFormatOptions,
|
|
507
|
+
): string {
|
|
508
|
+
const tag = themeFormatTag(locale, (l) => Intl.RelativeTimeFormat.supportedLocalesOf(l));
|
|
509
|
+
try {
|
|
510
|
+
return new Intl.RelativeTimeFormat(tag, { numeric: 'auto', ...options }).format(value, unit);
|
|
511
|
+
} catch {
|
|
512
|
+
try {
|
|
513
|
+
return new Intl.RelativeTimeFormat(ENGLISH_DISPLAY_LOCALE, {
|
|
514
|
+
numeric: 'auto',
|
|
515
|
+
...options,
|
|
516
|
+
}).format(value, unit);
|
|
517
|
+
} catch {
|
|
518
|
+
return `${value} ${unit}`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
356
521
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth error English pins.
|
|
3
|
+
*
|
|
4
|
+
* `mapEmailError` maps SDK failure codes to `t('auth.error.*')` strings, and
|
|
5
|
+
* three more error keys are set directly in the provider. Every English value
|
|
6
|
+
* below is resolved through the REAL default dictionary, so renaming a key or
|
|
7
|
+
* changing any English value fails here.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { QueekSdkError } from '@queekai/client-sdk';
|
|
11
|
+
import { t, defaultThemeStrings } from '../strings/theme-strings';
|
|
12
|
+
import { mapEmailError } from '../hooks/use-auth-flow';
|
|
13
|
+
|
|
14
|
+
const tt = (key: string): string => t(defaultThemeStrings, key);
|
|
15
|
+
|
|
16
|
+
function sdkError(code: string, message: string): QueekSdkError {
|
|
17
|
+
return new QueekSdkError(message, code, 400);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('mapEmailError code -> English pins', () => {
|
|
21
|
+
it.each([
|
|
22
|
+
['invalid_credentials', 'Wrong email or password. Please try again.'],
|
|
23
|
+
['login_mode_mismatch', 'This account uses a different sign-in method (Google or phone).'],
|
|
24
|
+
['account_already_exists', 'Email already registered. Please sign in instead.'],
|
|
25
|
+
['phone_already_exists', 'Phone number already in use.'],
|
|
26
|
+
['otp_resend_too_soon', 'Please wait before requesting another code.'],
|
|
27
|
+
['invalid_otp', 'Incorrect code. Please try again.'],
|
|
28
|
+
] as Array<[string, string]>)('code %s maps to its exact English', (code, english) => {
|
|
29
|
+
expect(mapEmailError(sdkError(code, 'server message'), tt)).toBe(english);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('passes an unknown-code server message through untouched', () => {
|
|
33
|
+
expect(mapEmailError(sdkError('something_else', 'Server says no.'), tt)).toBe('Server says no.');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('falls back to generic English when the server message is empty', () => {
|
|
37
|
+
expect(mapEmailError(sdkError('something_else', ''), tt)).toBe('Something went wrong. Please try again.');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('falls back to generic English for non-SDK failures', () => {
|
|
41
|
+
expect(mapEmailError(new Error('boom'), tt)).toBe('Something went wrong. Please try again.');
|
|
42
|
+
expect(mapEmailError('boom', tt)).toBe('Something went wrong. Please try again.');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('direct-set auth.error.* English pins', () => {
|
|
47
|
+
it.each([
|
|
48
|
+
['auth.error.firstname', 'Please enter your first name.'],
|
|
49
|
+
['auth.error.register', 'Registration failed. Try again.'],
|
|
50
|
+
['auth.error.google', 'Could not connect to Google sign-in. Please try again.'],
|
|
51
|
+
['auth.error.generic', 'Something went wrong. Please try again.'],
|
|
52
|
+
] as Array<[string, string]>)('%s resolves to its exact English', (key, english) => {
|
|
53
|
+
expect(tt(key)).toBe(english);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -6,13 +6,12 @@ import { extractProducts, normalizeProduct } from '../utils/product-normalizer';
|
|
|
6
6
|
import { useCartStore } from '../stores/cart-store';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* List-level `meta.currency` stamps the PLATFORM default (NGN) even on
|
|
9
|
+
* Client readers do not use `meta.currency`. List-level `meta.currency` stamps the PLATFORM default (NGN) even on
|
|
11
10
|
* non-NGN stores, so the item's own `currency` code is the source and the
|
|
12
11
|
* vendor's currency is the store-wide source. Missing codes fall back to the
|
|
13
12
|
* platform default as a LAST resort, in exactly one place.
|
|
14
13
|
*/
|
|
15
|
-
describe('currency
|
|
14
|
+
describe('currency resolution', () => {
|
|
16
15
|
it('exposes NGN as the platform default last resort', () => {
|
|
17
16
|
expect(PLATFORM_DEFAULT_CURRENCY).toBe('NGN');
|
|
18
17
|
});
|