@jotul/jotul-widgets 1.0.1 → 1.0.2

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.
@@ -1,8 +1,8 @@
1
1
  import './JotulWidget.css';
2
2
  import { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode } from './api';
3
3
  import type { JotulWidgetProps } from './types';
4
- export { normalizeWidgetLocale } from './i18n/widgetStrings';
4
+ export { DEFAULT_WIDGET_LOCALE_TAG, normalizeWidgetLocale, resolveWidgetUiLocale, } from './i18n/widgetStrings';
5
5
  export type { JotulWidgetLocale } from './i18n/widgetStrings';
6
6
  export { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, };
7
7
  export type { CheckWidgetAuthorizationOptions, DealerSearchResponse, JotulWidgetButtonStyling, JotulWidgetProps, JotulWidgetStyling, JotulWidgetType, WidgetAuthClientResponse, } from './types';
8
- export declare function JotulWidget({ type, endpoint, className, productName, locale: localeProp, brands, styling, }: JotulWidgetProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function JotulWidget({ type, endpoint, className, productName, locale: localeProp, market: marketProp, brands, styling, }: JotulWidgetProps): import("react/jsx-runtime").JSX.Element;
@@ -5,11 +5,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { FinderSearchRowSkeleton } from './components/FinderSearchRowSkeleton';
6
6
  import { ProductPageWidget } from './widgets/ProductPageWidget';
7
7
  import { ButtonSpinner } from './icons/ButtonSpinner';
8
- import { normalizeWidgetLocale, WIDGET_STRINGS } from './i18n/widgetStrings';
8
+ import { DEFAULT_WIDGET_LOCALE_TAG, resolveWidgetUiLocale, WIDGET_STRINGS, } from './i18n/widgetStrings';
9
9
  import { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, } from './api';
10
10
  import { createInquiryFormValues, getSafeWidgetErrorMessage, isValidEmail, isWidgetType, renderReadyState, } from './utils';
11
11
  import { getWidgetPrimaryButtonPresentation } from './utils/widgetPrimaryButtonPresentation';
12
- export { normalizeWidgetLocale } from './i18n/widgetStrings';
12
+ export { DEFAULT_WIDGET_LOCALE_TAG, normalizeWidgetLocale, resolveWidgetUiLocale, } from './i18n/widgetStrings';
13
13
  export { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, };
14
14
  const GEO_PERMISSION_DENIED = 1;
15
15
  const GEO_POSITION_UNAVAILABLE = 2;
@@ -19,9 +19,17 @@ const GEOLOCATION_OPTIONS = {
19
19
  timeout: 15000,
20
20
  maximumAge: 300000,
21
21
  };
22
- export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, productName, locale: localeProp, brands, styling, }) {
23
- const resolvedLocale = useMemo(() => normalizeWidgetLocale(localeProp), [localeProp]);
24
- const t = WIDGET_STRINGS[resolvedLocale];
22
+ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, productName, locale: localeProp, market: marketProp, brands, styling, }) {
23
+ const resolvedUiLocale = useMemo(() => resolveWidgetUiLocale(localeProp), [localeProp]);
24
+ const t = WIDGET_STRINGS[resolvedUiLocale];
25
+ const apiLocaleTag = useMemo(() => (localeProp?.trim() ? localeProp.trim() : DEFAULT_WIDGET_LOCALE_TAG), [localeProp]);
26
+ const apiMarket = useMemo(() => {
27
+ const m = marketProp?.trim();
28
+ if (!m)
29
+ return undefined;
30
+ const upper = m.toUpperCase();
31
+ return /^[A-Z]{2}$/.test(upper) ? upper : undefined;
32
+ }, [marketProp]);
25
33
  const heroPrimaryButton = useMemo(() => getWidgetPrimaryButtonPresentation(styling?.button, 'hero', {
26
34
  disabledWait: true,
27
35
  }), [styling?.button]);
@@ -44,9 +52,10 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
44
52
  const autocompleteCacheRef = useRef(new Map());
45
53
  const dealerSearchOptions = useMemo(() => ({
46
54
  endpoint,
47
- locale: resolvedLocale,
55
+ locale: apiLocaleTag,
56
+ market: apiMarket,
48
57
  brands,
49
- }), [brands, endpoint, resolvedLocale]);
58
+ }), [apiLocaleTag, apiMarket, brands, endpoint]);
50
59
  const runDealerSearchByCoordinates = useCallback(async (latitude, longitude) => {
51
60
  setLocationError(null);
52
61
  setSearchResult(null);
@@ -60,7 +69,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
60
69
  }
61
70
  }, [dealerSearchOptions]);
62
71
  const runLocationSearch = useCallback(() => {
63
- const messages = WIDGET_STRINGS[resolvedLocale];
72
+ const messages = WIDGET_STRINGS[resolvedUiLocale];
64
73
  setShouldAutoLocateAfterAuth(false);
65
74
  setLocationError(null);
66
75
  setSearchResult(null);
@@ -92,7 +101,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
92
101
  setLocationError(messages.locationGenericFailure);
93
102
  }
94
103
  }, GEOLOCATION_OPTIONS);
95
- }, [resolvedLocale, runDealerSearchByCoordinates]);
104
+ }, [resolvedUiLocale, runDealerSearchByCoordinates]);
96
105
  useEffect(() => {
97
106
  const query = locationQuery.trim();
98
107
  if (query.length < 3) {
@@ -100,7 +109,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
100
109
  setLocationSuggestions([]);
101
110
  return;
102
111
  }
103
- const cacheKey = query.toLowerCase();
112
+ const cacheKey = `${apiLocaleTag}|${apiMarket ?? ''}|${query.toLowerCase()}`;
104
113
  const cached = autocompleteCacheRef.current.get(cacheKey);
105
114
  if (cached != null) {
106
115
  setLocationSuggestions(cached);
@@ -121,7 +130,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
121
130
  cancelled = true;
122
131
  clearTimeout(timer);
123
132
  };
124
- }, [dealerSearchOptions, locationQuery]);
133
+ }, [apiLocaleTag, apiMarket, dealerSearchOptions, locationQuery]);
125
134
  useEffect(() => {
126
135
  if (type === 'productPage' && !isOpen)
127
136
  return;
package/dist/api.js CHANGED
@@ -1,5 +1,19 @@
1
+ import { DEFAULT_WIDGET_LOCALE_TAG } from './i18n/widgetStrings';
1
2
  /** Client-side default when JSON parse fails (English; localized in UI). */
2
3
  export const GENERIC_WIDGET_ERROR = 'Dealer finder is currently unavailable. Please try again later.';
4
+ function appendLocaleAndMarket(params, options) {
5
+ const locale = options?.locale != null && options.locale.trim() !== ''
6
+ ? options.locale.trim()
7
+ : DEFAULT_WIDGET_LOCALE_TAG;
8
+ params.set('locale', locale);
9
+ const market = options?.market?.trim();
10
+ if (market) {
11
+ const upper = market.toUpperCase();
12
+ if (/^[A-Z]{2}$/.test(upper)) {
13
+ params.set('market', upper);
14
+ }
15
+ }
16
+ }
3
17
  export async function checkWidgetAuthorization(options) {
4
18
  const endpoint = options?.endpoint ?? '/api/jotul/widget';
5
19
  const fetcher = options?.fetcher ?? fetch;
@@ -25,9 +39,7 @@ export async function searchDealersByPostalCode(postalCode, options) {
25
39
  const endpoint = options?.endpoint ?? '/api/jotul/widget';
26
40
  const fetcher = options?.fetcher ?? fetch;
27
41
  const params = new URLSearchParams({ postalCode: postalCode.trim() });
28
- if (options?.locale != null && options.locale.trim() !== '') {
29
- params.set('locale', options.locale.trim());
30
- }
42
+ appendLocaleAndMarket(params, options);
31
43
  if (Array.isArray(options?.brands)) {
32
44
  for (const brand of options.brands) {
33
45
  const value = brand.trim();
@@ -60,9 +72,7 @@ export async function searchDealersByCoordinates(latitude, longitude, options) {
60
72
  latitude: String(latitude),
61
73
  longitude: String(longitude),
62
74
  });
63
- if (options?.locale != null && options.locale.trim() !== '') {
64
- params.set('locale', options.locale.trim());
65
- }
75
+ appendLocaleAndMarket(params, options);
66
76
  if (Array.isArray(options?.brands)) {
67
77
  for (const brand of options.brands) {
68
78
  const value = brand.trim();
@@ -95,9 +105,7 @@ export async function searchLocationSuggestions(query, options) {
95
105
  type: 'autocomplete',
96
106
  q: query.trim(),
97
107
  });
98
- if (options?.locale != null && options.locale.trim() !== '') {
99
- params.set('locale', options.locale.trim());
100
- }
108
+ appendLocaleAndMarket(params, options);
101
109
  let response;
102
110
  try {
103
111
  response = await fetcher(`${endpoint}?${params.toString()}`, {
@@ -1,4 +1,6 @@
1
- export type JotulWidgetLocale = 'no' | 'se';
1
+ export type JotulWidgetLocale = 'no' | 'se' | 'en' | 'fr';
2
+ /** Default BCP 47 tag used for API `locale` when the prop is omitted. */
3
+ export declare const DEFAULT_WIDGET_LOCALE_TAG = "nb-NO";
2
4
  export type WidgetStrings = {
3
5
  genericWidgetError: string;
4
6
  invalidPostcodeError: string;
@@ -36,4 +38,9 @@ export type WidgetStrings = {
36
38
  readyWarrantyForm: string;
37
39
  };
38
40
  export declare const WIDGET_STRINGS: Record<JotulWidgetLocale, WidgetStrings>;
39
- export declare function normalizeWidgetLocale(raw: string | undefined): JotulWidgetLocale;
41
+ /**
42
+ * Maps `locale` prop: BCP 47 (e.g. `nb-NO`, `en-US`, `fr-FR`) or legacy shorthands (`no`, `sv`).
43
+ */
44
+ export declare function resolveWidgetUiLocale(raw: string | undefined): JotulWidgetLocale;
45
+ /** @deprecated Use `resolveWidgetUiLocale`. */
46
+ export declare const normalizeWidgetLocale: typeof resolveWidgetUiLocale;
@@ -1,3 +1,5 @@
1
+ /** Default BCP 47 tag used for API `locale` when the prop is omitted. */
2
+ export const DEFAULT_WIDGET_LOCALE_TAG = 'nb-NO';
1
3
  export const WIDGET_STRINGS = {
2
4
  no: {
3
5
  genericWidgetError: 'Forhandlerkartet er ikke tilgjengelig akkurat nå. Prøv igjen senere.',
@@ -71,17 +73,112 @@ export const WIDGET_STRINGS = {
71
73
  readyDealerFinder: 'JotulWidget klar: dealerFinder',
72
74
  readyWarrantyForm: 'JotulWidget klar: warrantyForm',
73
75
  },
76
+ en: {
77
+ genericWidgetError: 'The dealer finder is unavailable right now. Please try again later.',
78
+ invalidPostcodeError: 'Enter a valid postcode.',
79
+ invalidWidgetTypeError: 'This widget is not available right now.',
80
+ locationSearchFailed: 'Location search could not be completed. Try again.',
81
+ locationUnavailableBrowser: 'Location is not available in this browser. Try another browser or device.',
82
+ locationPermissionDenied: 'Location access was denied. Allow location for this site to find nearby dealers.',
83
+ locationPositionUnavailable: 'Your position could not be determined. Try again in a moment.',
84
+ locationTimeout: 'Finding your location took too long. Try again.',
85
+ locationGenericFailure: 'Could not read your location. Try again.',
86
+ locationTypeMore: 'Type at least 3 characters to search for an address.',
87
+ locationNoResults: 'No addresses found. Try a different search.',
88
+ locationManualHint: 'Enter your address to find dealers nearby',
89
+ findDealer: 'Find a dealer',
90
+ loading: 'Loading …',
91
+ useLocationHint: 'Use your location to find dealers nearby',
92
+ find: 'Locate',
93
+ finding: 'Searching …',
94
+ inquirySentSuccess: 'Your request has been sent.',
95
+ goBack: 'Back',
96
+ sendInquiryTitle: 'Request',
97
+ sendInquiryTitleWithProduct: 'Request regarding {product}',
98
+ dealerWillContact: '{dealer} will contact you as soon as possible.',
99
+ fieldName: 'Name',
100
+ fieldEmail: 'Email',
101
+ fieldPhone: 'Phone',
102
+ fieldComment: 'Comment',
103
+ formValidationRequired: 'Enter name, email, and phone.',
104
+ formValidationEmail: 'Enter a valid email address.',
105
+ dealersNearYou: '{count} dealers nearby',
106
+ unknownDealer: 'Unknown dealer',
107
+ sendInquiryCta: 'Send request',
108
+ sendInquiryEditing: 'Editing request',
109
+ readyDealerFinder: 'JotulWidget ready: dealerFinder',
110
+ readyWarrantyForm: 'JotulWidget ready: warrantyForm',
111
+ },
112
+ fr: {
113
+ genericWidgetError: "Le recherche de revendeurs n'est pas disponible pour le moment. Réessayez plus tard.",
114
+ invalidPostcodeError: 'Saisissez un code postal valide.',
115
+ invalidWidgetTypeError: "Ce widget n'est pas disponible pour le moment.",
116
+ locationSearchFailed: 'La recherche de lieu a échoué. Réessayez.',
117
+ locationUnavailableBrowser: "La position n'est pas disponible dans ce navigateur. Essayez un autre navigateur ou appareil.",
118
+ locationPermissionDenied: "L'accès à la position a été refusé. Autorisez la position pour ce site afin de trouver les revendeurs proches.",
119
+ locationPositionUnavailable: "Votre position n'a pas pu être déterminée. Réessayez dans un instant.",
120
+ locationTimeout: 'La recherche de votre position a pris trop de temps. Réessayez.',
121
+ locationGenericFailure: 'Impossible de lire votre position. Réessayez.',
122
+ locationTypeMore: 'Saisissez au moins 3 caractères pour chercher une adresse.',
123
+ locationNoResults: 'Aucune adresse trouvée. Essayez une autre recherche.',
124
+ locationManualHint: 'Saisissez votre adresse pour trouver des revendeurs à proximité',
125
+ findDealer: 'Trouver un revendeur',
126
+ loading: 'Chargement …',
127
+ useLocationHint: 'Utilisez votre position pour trouver des revendeurs à proximité',
128
+ find: 'Localiser',
129
+ finding: 'Recherche …',
130
+ inquirySentSuccess: 'Votre demande a été envoyée.',
131
+ goBack: 'Retour',
132
+ sendInquiryTitle: 'Demande',
133
+ sendInquiryTitleWithProduct: 'Demande concernant {product}',
134
+ dealerWillContact: '{dealer} vous contactera dès que possible.',
135
+ fieldName: 'Nom',
136
+ fieldEmail: 'E-mail',
137
+ fieldPhone: 'Téléphone',
138
+ fieldComment: 'Commentaire',
139
+ formValidationRequired: 'Renseignez le nom, l’e-mail et le téléphone.',
140
+ formValidationEmail: 'Saisissez une adresse e-mail valide.',
141
+ dealersNearYou: '{count} revendeurs à proximité',
142
+ unknownDealer: 'Revendeur inconnu',
143
+ sendInquiryCta: 'Envoyer la demande',
144
+ sendInquiryEditing: 'Modification de la demande',
145
+ readyDealerFinder: 'JotulWidget prêt : dealerFinder',
146
+ readyWarrantyForm: 'JotulWidget prêt : warrantyForm',
147
+ },
74
148
  };
75
- export function normalizeWidgetLocale(raw) {
149
+ /**
150
+ * Maps `locale` prop: BCP 47 (e.g. `nb-NO`, `en-US`, `fr-FR`) or legacy shorthands (`no`, `sv`).
151
+ */
152
+ export function resolveWidgetUiLocale(raw) {
76
153
  if (raw == null || raw.trim() === '') {
77
154
  return 'no';
78
155
  }
79
- const n = raw.trim().toLowerCase();
80
- if (n === 'no' || n === 'nb' || n === 'nn') {
156
+ const s = raw.trim();
157
+ const lower = s.toLowerCase();
158
+ if (lower === 'no' || lower === 'nb' || lower === 'nn') {
81
159
  return 'no';
82
160
  }
83
- if (n === 'se' || n === 'sv') {
161
+ if (lower === 'se' || lower === 'sv') {
84
162
  return 'se';
85
163
  }
86
- return 'no';
164
+ try {
165
+ const loc = new Intl.Locale(s.replace(/_/g, '-'));
166
+ const lang = (loc.language ?? '').toLowerCase();
167
+ if (lang === 'nb' || lang === 'nn' || lang === 'no')
168
+ return 'no';
169
+ if (lang === 'sv')
170
+ return 'se';
171
+ if (lang === 'en')
172
+ return 'en';
173
+ if (lang === 'fr')
174
+ return 'fr';
175
+ if (lang === 'de')
176
+ return 'en';
177
+ return 'no';
178
+ }
179
+ catch {
180
+ return 'no';
181
+ }
87
182
  }
183
+ /** @deprecated Use `resolveWidgetUiLocale`. */
184
+ export const normalizeWidgetLocale = resolveWidgetUiLocale;
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { checkWidgetAuthorization, normalizeWidgetLocale, searchDealersByCoordinates, searchDealersByPostalCode, JotulWidget, type CheckWidgetAuthorizationOptions, type DealerSearchResponse, type JotulWidgetButtonStyling, type JotulWidgetLocale, type JotulWidgetProps, type JotulWidgetStyling, type JotulWidgetType, type WidgetAuthClientResponse, } from './JotulWidget';
1
+ export { checkWidgetAuthorization, DEFAULT_WIDGET_LOCALE_TAG, normalizeWidgetLocale, resolveWidgetUiLocale, searchDealersByCoordinates, searchDealersByPostalCode, JotulWidget, type CheckWidgetAuthorizationOptions, type DealerSearchResponse, type JotulWidgetButtonStyling, type JotulWidgetLocale, type JotulWidgetProps, type JotulWidgetStyling, type JotulWidgetType, type WidgetAuthClientResponse, } from './JotulWidget';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  'use client';
2
- export { checkWidgetAuthorization, normalizeWidgetLocale, searchDealersByCoordinates, searchDealersByPostalCode, JotulWidget, } from './JotulWidget';
2
+ export { checkWidgetAuthorization, DEFAULT_WIDGET_LOCALE_TAG, normalizeWidgetLocale, resolveWidgetUiLocale, searchDealersByCoordinates, searchDealersByPostalCode, JotulWidget, } from './JotulWidget';
package/dist/types.d.ts CHANGED
@@ -37,7 +37,10 @@ export type LocationAutocompleteResponse = {
37
37
  export type CheckWidgetAuthorizationOptions = {
38
38
  endpoint?: string;
39
39
  fetcher?: typeof fetch;
40
+ /** BCP 47 language tag (e.g. `nb-NO`, `en-US`) sent to the API as `locale`. */
40
41
  locale?: string;
42
+ /** ISO 3166-1 alpha-2 market/country (e.g. `NO`, `SE`) sent to the API as `market`. */
43
+ market?: string;
41
44
  brands?: string[];
42
45
  };
43
46
  /** CSS values for primary CTA buttons (find dealer, send inquiry, etc.). */
@@ -54,7 +57,10 @@ export type JotulWidgetProps = {
54
57
  endpoint?: string;
55
58
  className?: string;
56
59
  productName?: string;
60
+ /** BCP 47 tag for widget UI strings and API `locale` (default `nb-NO` when omitted). */
57
61
  locale?: string;
62
+ /** ISO 3166-1 alpha-2 market filter for dealers and geocoder country bias (`NO`, `SE`, …). */
63
+ market?: string;
58
64
  brands?: string[];
59
65
  styling?: JotulWidgetStyling;
60
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jotul/jotul-widgets",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [