@wocart/storefront 2.1.0 → 2.2.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
5
5
  in a new major version, and an export is marked `@deprecated` for at least one
6
6
  minor release before a major removes it.
7
7
 
8
+ ## 2.2.0 — 2026-11-01
9
+
10
+ ### Added
11
+
12
+ - `localWhatsAppWidget(config)`: the WhatsApp widget as a page in
13
+ `config.locale` shows it. Its texts are written once, in the store's
14
+ primary language; on a page in another language they are null, so a theme
15
+ shows its own words in the shopper's language instead of the merchant's in
16
+ another one. The agent's name and every setting stay.
17
+
18
+ ### Fixed
19
+
20
+ - `formatMoney` writes the currency symbol after the amount in every
21
+ language, as Algerian prices are written (`5 000 DA`, `5,000 DA`,
22
+ `5.000 دج`), joined by a no-break space so the two never wrap apart. French
23
+ and English put it before the amount (`DA 5 000`). Without a symbol it is
24
+ the amount alone, with no stray space.
25
+
8
26
  ## 2.1.0 — 2026-10-31
9
27
 
10
28
  ### Added
package/USAGE.md CHANGED
@@ -223,7 +223,7 @@ server directly.
223
223
  | `isWilayaId` | Whether a value is a wilaya id — the range `WilayaIdSchema` checks on the server, without a schema in the browser |
224
224
  | `readShippingRates`, `readCouponOutcome` | Read in the browser what the server handed it — the rates a page carries, a quote's coupon outcome — accepting exactly what `ShippingRatesSchema` / `CouponOutcomeSchema` accept, without Zod |
225
225
  | `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
226
- | `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | WhatsApp prefill text and the `wa.me` link |
226
+ | `localWhatsAppWidget`, `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | the widget in the page's language, WhatsApp prefill text and the `wa.me` link |
227
227
  | `storefrontPath`, `matchStorefrontPath`, `STOREFRONT_PAGE_PATHS` | The page types and their public paths: build a link, recognise a page |
228
228
  | `showsWidgetOn`, `STOREFRONT_ROUTES` | Which widget surface a pathname is (`STOREFRONT_ROUTES` deprecated) |
229
229
  | `productImageSource` | Which photo represents a product (cover → gallery fallback) |
@@ -1926,21 +1926,30 @@ already written, and the thank-you page offers the same handoff for an order.
1926
1926
  The template belongs to the merchant — the dashboard authors and previews it
1927
1927
  within the platform's limits — while the page resolves it:
1928
1928
 
1929
+ The merchant writes the widget's texts once, in the store's primary language.
1930
+ Read the widget with `localWhatsAppWidget(config)`: on a page in another
1931
+ language its texts (labels, welcome, prefills) are null, so the theme shows its
1932
+ own words in the shopper's language; the agent's name and the settings stay.
1933
+
1929
1934
  ~~~ts
1930
1935
  import {
1931
1936
  buildWhatsAppHref,
1937
+ localWhatsAppWidget,
1932
1938
  resolvePrefillText,
1933
1939
  } from "@wocart/storefront";
1934
1940
 
1941
+ const widget = localWhatsAppWidget(config);
1942
+ if (!widget) return;
1943
+
1935
1944
  const text = resolvePrefillText({
1936
- template: config.whatsapp.prefillProduct ?? "Hello {store}, about {product}: {url}",
1945
+ template: widget.prefillProduct ?? "Hello {store}, about {product}: {url}",
1937
1946
  storeName: config.name,
1938
1947
  productName: product.name, // falls back to the store name when absent
1939
1948
  url: Astro.url.href, // ad-click parameters (`utm_*`, `fbclid`) stripped
1940
1949
  orderNumber: null, // resolves to nothing when absent
1941
1950
  });
1942
1951
 
1943
- const href = buildWhatsAppHref(config.whatsapp.href, text);
1952
+ const href = buildWhatsAppHref(widget.href, text);
1944
1953
  ~~~
1945
1954
 
1946
1955
  Three rules come with it, and a theme must not re-implement them:
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from
15
15
  export type { MediaDelivery, MediaDeliveryKind, MediaImage, MediaImageParams, MediaImageSrcsetOptions, PickedImage, Picture } from "./utils/image.js";
16
16
  export { productImageSource } from "./utils/product-image.js";
17
17
  export type { ProductImageSourceInput } from "./utils/product-image.js";
18
- export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
18
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
19
19
  export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
20
20
  export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
21
21
  export { servedLanguage } from "./locales.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  export { formatMoney } from "./utils/money.js";
13
13
  export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from "./utils/image.js";
14
14
  export { productImageSource } from "./utils/product-image.js";
15
- export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
15
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
16
16
  export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
17
17
  export { servedLanguage } from "./locales.js";
18
18
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
@@ -2,7 +2,7 @@
2
2
  * Deep Money Formatter
3
3
  *
4
4
  * One formatter for every price a storefront shows.
5
- * Handles locale-aware formatting, RTL symbol positioning, and Arabic-Indic digits.
5
+ * Handles locale-aware formatting and Arabic-Indic digits; the symbol follows the amount.
6
6
  *
7
7
  * Design principle: ONE function that handles all cases. Callers curry if needed.
8
8
  */
@@ -26,14 +26,14 @@ export interface MoneyFormatOptions {
26
26
  * // Arabic: 5,000 دج (with Arabic-Indic digits)
27
27
  * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
28
28
  *
29
- * // French: DA 5 000,00
30
- * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
29
+ * // French: 5 000,00 DA
30
+ * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR', decimals: 2 })
31
31
  *
32
- * // English: DA 5,000.00
32
+ * // English: 5,000 DA
33
33
  * formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
34
- *
35
- * // No decimals: 5,000 دج
36
- * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
37
34
  * ```
35
+ *
36
+ * The symbol follows the amount in every language, as Algerian prices are
37
+ * written, joined by a no-break space so the two never wrap apart.
38
38
  */
39
39
  export declare function formatMoney(amount: number, options: MoneyFormatOptions): string;
@@ -2,7 +2,7 @@
2
2
  * Deep Money Formatter
3
3
  *
4
4
  * One formatter for every price a storefront shows.
5
- * Handles locale-aware formatting, RTL symbol positioning, and Arabic-Indic digits.
5
+ * Handles locale-aware formatting and Arabic-Indic digits; the symbol follows the amount.
6
6
  *
7
7
  * Design principle: ONE function that handles all cases. Callers curry if needed.
8
8
  */
@@ -18,15 +18,15 @@
18
18
  * // Arabic: 5,000 دج (with Arabic-Indic digits)
19
19
  * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
20
20
  *
21
- * // French: DA 5 000,00
22
- * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
21
+ * // French: 5 000,00 DA
22
+ * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR', decimals: 2 })
23
23
  *
24
- * // English: DA 5,000.00
24
+ * // English: 5,000 DA
25
25
  * formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
26
- *
27
- * // No decimals: 5,000 دج
28
- * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
29
26
  * ```
27
+ *
28
+ * The symbol follows the amount in every language, as Algerian prices are
29
+ * written, joined by a no-break space so the two never wrap apart.
30
30
  */
31
31
  export function formatMoney(amount, options) {
32
32
  const { symbol, locale, decimals = 0 } = options;
@@ -35,9 +35,5 @@ export function formatMoney(amount, options) {
35
35
  minimumFractionDigits: decimals,
36
36
  maximumFractionDigits: decimals,
37
37
  });
38
- // RTL-aware symbol positioning
39
- // Arabic: suffix (5,000.00 دج)
40
- // French/English: prefix (DA 5,000.00)
41
- const isRTL = locale.startsWith("ar");
42
- return isRTL ? `${formatted} ${symbol}` : `${symbol} ${formatted}`;
38
+ return symbol ? `${formatted}\u00A0${symbol}` : formatted;
43
39
  }
@@ -16,6 +16,7 @@
16
16
  * 3. **The encoded text is appended the same way everywhere**, so a base href
17
17
  * that already carries a query string keeps it.
18
18
  */
19
+ import type { StoreConfig, StoreWhatsAppWidget } from "./types.js";
19
20
  /**
20
21
  * The hard cap on a resolved prefill message, in characters.
21
22
  *
@@ -53,3 +54,10 @@ export interface ResolvePrefillParams {
53
54
  export declare function resolvePrefillText({ template, storeName, productName, url, orderNumber, }: ResolvePrefillParams): string;
54
55
  /** Appends the resolved message to a `wa.me` href, encoded and query-safe. */
55
56
  export declare function buildWhatsAppHref(baseHref: string, text: string): string;
57
+ /**
58
+ * The WhatsApp widget as a page in `config.locale` shows it. The merchant
59
+ * writes its texts once, in the store's primary language (`config.lang`), so
60
+ * a page in another language gets them as null and shows the theme's own
61
+ * words in its language instead; the agent's name and every setting stay.
62
+ */
63
+ export declare function localWhatsAppWidget(config: Pick<StoreConfig, "whatsapp" | "lang" | "locale">): StoreWhatsAppWidget | null;
package/dist/whatsapp.js CHANGED
@@ -1,21 +1,3 @@
1
- /**
2
- * WhatsApp prefill messages — the storefront's message rule.
3
- *
4
- * A storefront's WhatsApp launcher hands the shopper to WhatsApp with the
5
- * message already written. The template belongs to the merchant (the dashboard
6
- * authors it within the limits below), and the page
7
- * resolves it: `{store}`, `{product}`, `{url}`, `{order}`.
8
- *
9
- * Three rules live here so no theme re-invents them:
10
- *
11
- * 1. **Ad-click parameters are stripped from `{url}`.** A shopper arriving from
12
- * a paid ad should not hand the merchant's WhatsApp a `fbclid`-laden link.
13
- * 2. **The resolved message is capped at `MAX_RESOLVED_PREFILL_LENGTH`, cut at
14
- * a word boundary.** Browsers and WhatsApp both truncate silently past a
15
- * point; a cut mid-word reads as a broken store.
16
- * 3. **The encoded text is appended the same way everywhere**, so a base href
17
- * that already carries a query string keeps it.
18
- */
19
1
  /**
20
2
  * The hard cap on a resolved prefill message, in characters.
21
3
  *
@@ -87,3 +69,17 @@ export function buildWhatsAppHref(baseHref, text) {
87
69
  const separator = baseHref.includes("?") ? "&" : "?";
88
70
  return `${baseHref}${separator}text=${encodeURIComponent(text)}`;
89
71
  }
72
+ /** The widget's texts the merchant writes once, in the store's primary language. */
73
+ const WIDGET_TEXTS = ["caption", "welcomeMessage", "launcherLabel", "ctaLabel", "prefillGeneral", "prefillProduct", "thankYouButtonLabel", "prefillThankYou"];
74
+ /**
75
+ * The WhatsApp widget as a page in `config.locale` shows it. The merchant
76
+ * writes its texts once, in the store's primary language (`config.lang`), so
77
+ * a page in another language gets them as null and shows the theme's own
78
+ * words in its language instead; the agent's name and every setting stay.
79
+ */
80
+ export function localWhatsAppWidget(config) {
81
+ const widget = config.whatsapp;
82
+ if (!widget || config.locale === config.lang)
83
+ return widget;
84
+ return { ...widget, ...Object.fromEntries(WIDGET_TEXTS.map((key) => [key, null])) };
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
5
5
  "keywords": [
6
6
  "wocart",