@proveanything/smartlinks 1.2.4 → 1.3.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.
package/dist/http.js CHANGED
@@ -2,6 +2,18 @@
2
2
  // This module replaces the ApiClient constructor. It keeps baseURL, apiKey, bearerToken
3
3
  // in module-scope variables, and provides a shared `request<T>(path)` helper that will
4
4
  // be used by all namespaced files (collection.ts, product.ts, etc.).
5
+ var __rest = (this && this.__rest) || function (s, e) {
6
+ var t = {};
7
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
8
+ t[p] = s[p];
9
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
10
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
11
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
12
+ t[p[i]] = s[p[i]];
13
+ }
14
+ return t;
15
+ };
16
+ import { SmartlinksApiError } from "./types/error";
5
17
  let baseURL = null;
6
18
  let apiKey = undefined;
7
19
  let bearerToken = undefined;
@@ -63,6 +75,88 @@ function safeBodyPreview(body) {
63
75
  }
64
76
  return body;
65
77
  }
78
+ /**
79
+ * Normalizes various server error response formats into a consistent ErrorResponse shape.
80
+ * Handles multiple formats:
81
+ * - { code: number, message: string } (standard)
82
+ * - { errorCode: string, errorText: string }
83
+ * - { error: string, message: string }
84
+ * - { error: string } (just error field)
85
+ * - { ok: false, error: string }
86
+ * - Plain string
87
+ *
88
+ * @param responseBody - The parsed JSON response body from the server
89
+ * @param statusCode - HTTP status code
90
+ * @returns Normalized ErrorResponse object
91
+ */
92
+ function normalizeErrorResponse(responseBody, statusCode) {
93
+ if (!responseBody || typeof responseBody !== 'object') {
94
+ // Plain string or non-object response
95
+ const message = typeof responseBody === 'string' ? responseBody : 'Request failed';
96
+ return {
97
+ code: statusCode,
98
+ message,
99
+ };
100
+ }
101
+ // Extract all possible fields from the response
102
+ const { code, errorCode, error, message, errorText, ok } = responseBody, rest = __rest(responseBody
103
+ // Determine the error code (prefer numeric code, fall back to errorCode or error string)
104
+ , ["code", "errorCode", "error", "message", "errorText", "ok"]);
105
+ // Determine the error code (prefer numeric code, fall back to errorCode or error string)
106
+ let normalizedCode = statusCode;
107
+ if (typeof code === 'number') {
108
+ normalizedCode = code;
109
+ }
110
+ else if (typeof errorCode === 'string' || typeof error === 'string') {
111
+ // Keep statusCode as numeric code, but preserve the string code in details
112
+ }
113
+ // Determine the error message
114
+ let normalizedMessage;
115
+ if (message) {
116
+ normalizedMessage = String(message);
117
+ }
118
+ else if (errorText) {
119
+ normalizedMessage = String(errorText);
120
+ }
121
+ else if (error) {
122
+ normalizedMessage = String(error);
123
+ }
124
+ else if (ok === false) {
125
+ normalizedMessage = 'Request failed';
126
+ }
127
+ else {
128
+ normalizedMessage = `Request failed with status ${statusCode}`;
129
+ }
130
+ // Extract the server-specific error code string (distinct from HTTP status code)
131
+ let normalizedErrorCode;
132
+ if (errorCode && typeof errorCode === 'string') {
133
+ normalizedErrorCode = errorCode;
134
+ }
135
+ else if (error && typeof error === 'string') {
136
+ normalizedErrorCode = error;
137
+ }
138
+ // Collect any additional details
139
+ const details = Object.assign({}, rest);
140
+ // Preserve error fields in details for backward compatibility
141
+ if (errorCode && typeof errorCode === 'string') {
142
+ details.errorCode = errorCode;
143
+ }
144
+ if (error && typeof error === 'string') {
145
+ details.error = error;
146
+ }
147
+ if (errorText && errorText !== normalizedMessage) {
148
+ details.errorText = errorText;
149
+ }
150
+ if (ok === false) {
151
+ details.ok = ok;
152
+ }
153
+ return {
154
+ code: normalizedCode,
155
+ errorCode: normalizedErrorCode,
156
+ message: normalizedMessage,
157
+ details: Object.keys(details).length > 0 ? details : undefined,
158
+ };
159
+ }
66
160
  /**
67
161
  * Call this once (e.g. at app startup) to configure baseURL/auth.
68
162
  *
@@ -336,14 +430,18 @@ export async function request(path) {
336
430
  });
337
431
  logDebug('[smartlinks] GET response', { url, status: response.status, ok: response.ok });
338
432
  if (!response.ok) {
339
- // Try to parse ErrorResponse; if that fails, throw generic
433
+ // Try to parse error response body and normalize it
434
+ let responseBody;
340
435
  try {
341
- const errBody = (await response.json());
342
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
436
+ responseBody = await response.json();
343
437
  }
344
438
  catch (_a) {
345
- throw new Error(`Request to ${url} failed with status ${response.status}`);
439
+ // Failed to parse JSON, use status code only
440
+ responseBody = null;
346
441
  }
442
+ const errBody = normalizeErrorResponse(responseBody, response.status);
443
+ const message = `Error ${errBody.code}: ${errBody.message}`;
444
+ throw new SmartlinksApiError(message, response.status, errBody, url);
347
445
  }
348
446
  return (await response.json());
349
447
  }
@@ -384,13 +482,16 @@ export async function post(path, body, extraHeaders) {
384
482
  });
385
483
  logDebug('[smartlinks] POST response', { url, status: response.status, ok: response.ok });
386
484
  if (!response.ok) {
485
+ let responseBody;
387
486
  try {
388
- const errBody = (await response.json());
389
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
487
+ responseBody = await response.json();
390
488
  }
391
489
  catch (_a) {
392
- throw new Error(`Request to ${url} failed with status ${response.status}`);
490
+ responseBody = null;
393
491
  }
492
+ const errBody = normalizeErrorResponse(responseBody, response.status);
493
+ const message = `Error ${errBody.code}: ${errBody.message}`;
494
+ throw new SmartlinksApiError(message, response.status, errBody, url);
394
495
  }
395
496
  return (await response.json());
396
497
  }
@@ -431,13 +532,16 @@ export async function put(path, body, extraHeaders) {
431
532
  });
432
533
  logDebug('[smartlinks] PUT response', { url, status: response.status, ok: response.ok });
433
534
  if (!response.ok) {
535
+ let responseBody;
434
536
  try {
435
- const errBody = (await response.json());
436
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
537
+ responseBody = await response.json();
437
538
  }
438
539
  catch (_a) {
439
- throw new Error(`Request to ${url} failed with status ${response.status}`);
540
+ responseBody = null;
440
541
  }
542
+ const errBody = normalizeErrorResponse(responseBody, response.status);
543
+ const message = `Error ${errBody.code}: ${errBody.message}`;
544
+ throw new SmartlinksApiError(message, response.status, errBody, url);
441
545
  }
442
546
  return (await response.json());
443
547
  }
@@ -478,13 +582,16 @@ export async function patch(path, body, extraHeaders) {
478
582
  });
479
583
  logDebug('[smartlinks] PATCH response', { url, status: response.status, ok: response.ok });
480
584
  if (!response.ok) {
585
+ let responseBody;
481
586
  try {
482
- const errBody = (await response.json());
483
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
587
+ responseBody = await response.json();
484
588
  }
485
589
  catch (_a) {
486
- throw new Error(`Request to ${url} failed with status ${response.status}`);
590
+ responseBody = null;
487
591
  }
592
+ const errBody = normalizeErrorResponse(responseBody, response.status);
593
+ const message = `Error ${errBody.code}: ${errBody.message}`;
594
+ throw new SmartlinksApiError(message, response.status, errBody, url);
488
595
  }
489
596
  return (await response.json());
490
597
  }
@@ -528,13 +635,16 @@ export async function requestWithOptions(path, options) {
528
635
  const response = await fetch(url, Object.assign(Object.assign({}, options), { headers }));
529
636
  logDebug('[smartlinks] requestWithOptions response', { url, status: response.status, ok: response.ok });
530
637
  if (!response.ok) {
638
+ let responseBody;
531
639
  try {
532
- const errBody = (await response.json());
533
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
640
+ responseBody = await response.json();
534
641
  }
535
642
  catch (_a) {
536
- throw new Error(`Request to ${url} failed with status ${response.status}`);
643
+ responseBody = null;
537
644
  }
645
+ const errBody = normalizeErrorResponse(responseBody, response.status);
646
+ const message = `Error ${errBody.code}: ${errBody.message}`;
647
+ throw new SmartlinksApiError(message, response.status, errBody, url);
538
648
  }
539
649
  return (await response.json());
540
650
  }
@@ -569,13 +679,16 @@ export async function del(path, extraHeaders) {
569
679
  });
570
680
  logDebug('[smartlinks] DELETE response', { url, status: response.status, ok: response.ok });
571
681
  if (!response.ok) {
682
+ let responseBody;
572
683
  try {
573
- const errBody = (await response.json());
574
- throw new Error(`Error ${errBody.code}: ${errBody.message}`);
684
+ responseBody = await response.json();
575
685
  }
576
686
  catch (_a) {
577
- throw new Error(`Request to ${url} failed with status ${response.status}`);
687
+ responseBody = null;
578
688
  }
689
+ const errBody = normalizeErrorResponse(responseBody, response.status);
690
+ const message = `Error ${errBody.code}: ${errBody.message}`;
691
+ throw new SmartlinksApiError(message, response.status, errBody, url);
579
692
  }
580
693
  // If the response is empty, just return undefined
581
694
  if (response.status === 204)
package/dist/i18n.md ADDED
@@ -0,0 +1,287 @@
1
+ # Internationalization (i18n) System
2
+
3
+ This document explains the i18n system for SmartLinks apps, covering URL-based language selection, static translations, and dynamic overrides.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ The i18n system supports:
10
+
11
+ - **URL parameter language selection** (`?lang=de`) - Ideal for iframe embedding
12
+ - **Static translations** in JSON files - Fast, no API calls
13
+ - **Dynamic overrides** via SmartLinks appConfig - Customer-configurable
14
+ - **Type-safe translation keys** - Compile-time safety
15
+ - **Widget support** - Translations can be passed to embedded widgets
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ ### Using Translations in Components
22
+
23
+ ```typescript
24
+ import { useLanguage } from '@/i18n';
25
+
26
+ export const MyComponent = () => {
27
+ const { t, lang } = useLanguage();
28
+
29
+ return (
30
+ <div>
31
+ <h1>{t('public.title')}</h1>
32
+ <p>{t('public.description')}</p>
33
+ <small>Language: {lang}</small>
34
+ </div>
35
+ );
36
+ };
37
+ ```
38
+
39
+ ### With Parameter Interpolation
40
+
41
+ ```typescript
42
+ const { t } = useLanguage();
43
+
44
+ // Translation: "Hello, {{name}}!"
45
+ t('greeting', { name: 'John' }); // "Hello, John!"
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Language Detection Priority
51
+
52
+ The system detects language from multiple sources (in order):
53
+
54
+ | Priority | Source | Example |
55
+ |----------|--------|---------|
56
+ | 1 | URL parameter | `?lang=de` |
57
+ | 2 | localStorage | `smartlinks-lang` key |
58
+ | 3 | Browser language | `navigator.language` |
59
+ | 4 | Default | `en` |
60
+
61
+ For iframe apps, the SmartLinks platform passes the language via URL parameter.
62
+
63
+ ---
64
+
65
+ ## File Structure
66
+
67
+ ```text
68
+ src/i18n/
69
+ ├── index.ts # Main exports
70
+ ├── LanguageContext.tsx # React context + provider
71
+ ├── types.ts # TypeScript types
72
+ └── locales/
73
+ ├── en.json # English translations
74
+ ├── de.json # German translations
75
+ └── fr.json # French translations
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Adding New Translations
81
+
82
+ ### 1. Add the Key to Types
83
+
84
+ ```typescript
85
+ // src/i18n/types.ts
86
+ export type TranslationKey =
87
+ | 'common.loading'
88
+ | 'common.error'
89
+ | 'myFeature.title' // Add new key
90
+ | 'myFeature.description';
91
+ ```
92
+
93
+ ### 2. Add Translations to All Locales
94
+
95
+ ```json
96
+ // src/i18n/locales/en.json
97
+ {
98
+ "myFeature.title": "My Feature",
99
+ "myFeature.description": "Description of my feature"
100
+ }
101
+
102
+ // src/i18n/locales/de.json
103
+ {
104
+ "myFeature.title": "Meine Funktion",
105
+ "myFeature.description": "Beschreibung meiner Funktion"
106
+ }
107
+ ```
108
+
109
+ ### 3. Use in Components
110
+
111
+ ```typescript
112
+ const { t } = useLanguage();
113
+ return <h1>{t('myFeature.title')}</h1>;
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Adding New Languages
119
+
120
+ ### 1. Create the Locale File
121
+
122
+ ```json
123
+ // src/i18n/locales/es.json
124
+ {
125
+ "common.loading": "Cargando...",
126
+ "common.error": "Algo salió mal",
127
+ // ... all other keys
128
+ }
129
+ ```
130
+
131
+ ### 2. Import in LanguageContext
132
+
133
+ ```typescript
134
+ // src/i18n/LanguageContext.tsx
135
+ import es from './locales/es.json';
136
+
137
+ const staticTranslations: Record<string, PartialTranslations> = {
138
+ en: en as PartialTranslations,
139
+ de: de as PartialTranslations,
140
+ fr: fr as PartialTranslations,
141
+ es: es as PartialTranslations, // Add new language
142
+ };
143
+
144
+ const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es']; // Add to list
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Dynamic Translations (SmartLinks appConfig)
150
+
151
+ For customer-configurable translations, store them in SmartLinks appConfig:
152
+
153
+ ### Config Structure
154
+
155
+ ```json
156
+ {
157
+ "i18n": {
158
+ "supportedLanguages": ["en", "de"],
159
+ "defaultLanguage": "en",
160
+ "translations": {
161
+ "en": {
162
+ "public.title": "Custom Brand Title",
163
+ "public.description": "Custom brand description"
164
+ },
165
+ "de": {
166
+ "public.title": "Benutzerdefinierter Markentitel",
167
+ "public.description": "Benutzerdefinierte Markenbeschreibung"
168
+ }
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### Loading Dynamic Translations
175
+
176
+ ```typescript
177
+ import { useCollectionAppConfig } from '@/hooks/useSmartLinksData';
178
+ import { LanguageProvider } from '@/i18n';
179
+
180
+ const App = () => {
181
+ const { data: config } = useCollectionAppConfig(collectionId, appId);
182
+
183
+ return (
184
+ <LanguageProvider dynamicTranslations={config?.i18n?.translations}>
185
+ <MyApp />
186
+ </LanguageProvider>
187
+ );
188
+ };
189
+ ```
190
+
191
+ Dynamic translations override static ones, so customers can customize specific strings while inheriting defaults.
192
+
193
+ ---
194
+
195
+ ## Widgets
196
+
197
+ Widgets receive language context via props rather than React context (since they may run outside the provider).
198
+
199
+ ### Widget Props
200
+
201
+ ```typescript
202
+ interface SmartLinksWidgetProps {
203
+ // ... other props
204
+ lang?: string;
205
+ translations?: Record<string, string>;
206
+ }
207
+ ```
208
+
209
+ ### Using in Widgets
210
+
211
+ ```typescript
212
+ import { createTranslator } from '@/i18n';
213
+
214
+ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
215
+ lang = 'en',
216
+ translations = {},
217
+ ...props
218
+ }) => {
219
+ const t = createTranslator(lang, translations);
220
+
221
+ return <div>{t('widget.title')}</div>;
222
+ };
223
+ ```
224
+
225
+ The `createTranslator` function creates a standalone translation function that doesn't require React context.
226
+
227
+ ---
228
+
229
+ ## URL Examples
230
+
231
+ ```
232
+ # English (default)
233
+ /#/?collectionId=abc&appId=pamphlet
234
+
235
+ # German
236
+ /#/?collectionId=abc&appId=pamphlet&lang=de
237
+
238
+ # French
239
+ /#/?collectionId=abc&appId=pamphlet&lang=fr
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Best Practices
245
+
246
+ 1. **Use dot notation** for key namespacing: `category.subcategory.key`
247
+ 2. **Keep translations flat** - avoid nested objects in locale files
248
+ 3. **Add keys to types.ts first** - ensures type safety
249
+ 4. **Provide fallbacks** - English is always the fallback language
250
+ 5. **Test all languages** - Use `?lang=xx` to switch during development
251
+
252
+ ---
253
+
254
+ ## API Reference
255
+
256
+ ### `useLanguage()`
257
+
258
+ Hook to access language context.
259
+
260
+ ```typescript
261
+ const {
262
+ lang, // Current language code
263
+ setLang, // Function to change language
264
+ t, // Translation function
265
+ supportedLanguages // Array of supported language codes
266
+ } = useLanguage();
267
+ ```
268
+
269
+ ### `createTranslator(lang, overrides)`
270
+
271
+ Creates a standalone translation function for widgets.
272
+
273
+ ```typescript
274
+ const t = createTranslator('de', { 'custom.key': 'Custom value' });
275
+ t('common.loading'); // "Laden..."
276
+ t('custom.key'); // "Custom value"
277
+ ```
278
+
279
+ ### `LanguageProvider`
280
+
281
+ React provider component.
282
+
283
+ ```typescript
284
+ <LanguageProvider dynamicTranslations={optionalOverrides}>
285
+ <App />
286
+ </LanguageProvider>
287
+ ```