@smileid/web-components 11.0.3 → 11.1.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/README.md +61 -0
- package/dist/components/smart-camera-web/src/README.md +0 -1
- package/dist/esm/{DocumentCaptureScreens-C5BhNB-0.js → DocumentCaptureScreens-DHAfE8VG.js} +191 -185
- package/dist/esm/DocumentCaptureScreens-DHAfE8VG.js.map +1 -0
- package/dist/esm/{EndUserConsent-D4fd1ovG.js → EndUserConsent-DE6OBSGG.js} +65 -63
- package/dist/esm/EndUserConsent-DE6OBSGG.js.map +1 -0
- package/dist/esm/{Navigation-CTjK6tLU.js → Navigation-kjll5SgE.js} +17 -9
- package/dist/esm/Navigation-kjll5SgE.js.map +1 -0
- package/dist/esm/{SelfieCaptureScreens-KoQpCxtc.js → SelfieCaptureScreens-DaREKFi4.js} +3087 -3159
- package/dist/esm/SelfieCaptureScreens-DaREKFi4.js.map +1 -0
- package/dist/esm/{TotpConsent-CQU5jQi4.js → TotpConsent-DsMJhXD8.js} +13 -9
- package/dist/esm/TotpConsent-DsMJhXD8.js.map +1 -0
- package/dist/esm/combobox.js +20 -19
- package/dist/esm/combobox.js.map +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/index-LJce6hoU.js +1360 -0
- package/dist/esm/index-LJce6hoU.js.map +1 -0
- package/dist/esm/localisation.js +21 -0
- package/dist/esm/localisation.js.map +1 -0
- package/dist/esm/main.js +34 -17
- package/dist/esm/main.js.map +1 -1
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/{package-B-UwEdv7.js → package-BoRHLVSv.js} +25 -40
- package/dist/esm/package-BoRHLVSv.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +22 -18
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +136 -152
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/src/components/combobox/src/index.js +424 -1
- package/dist/src/components/document/src/index.js +1422 -1
- package/dist/src/components/end-user-consent/src/index.js +1573 -1
- package/dist/src/components/selfie/src/index.js +1220 -1
- package/dist/src/components/signature-pad/src/index.js +787 -1
- package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js +2753 -1
- package/dist/src/components/totp-consent/src/index.js +1292 -1
- package/dist/types/combobox.d.ts +2 -2
- package/dist/types/document.d.ts +2 -2
- package/dist/types/end-user-consent.d.ts +2 -2
- package/dist/types/locale.d.ts +19 -0
- package/dist/types/localisation.d.ts +21 -0
- package/dist/types/main.d.ts +35 -26
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/selfie.d.ts +2 -2
- package/dist/types/signature-pad.d.ts +2 -2
- package/dist/types/smart-camera-web.d.ts +2 -2
- package/dist/types/totp-consent.d.ts +2 -2
- package/lib/components/camera-permission/CameraPermission.js +8 -3
- package/lib/components/combobox/src/Combobox.js +4 -2
- package/lib/components/document/src/DocumentCaptureScreens.js +4 -3
- package/lib/components/document/src/DocumentCaptureScreens.stories.js +37 -13
- package/lib/components/document/src/document-capture/DocumentCapture.js +20 -14
- package/lib/components/document/src/document-capture/DocumentCapture.stories.js +11 -2
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +16 -11
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +14 -5
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +11 -7
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.stories.js +14 -5
- package/lib/components/end-user-consent/src/EndUserConsent.js +30 -29
- package/lib/components/end-user-consent/src/EndUserConsent.stories.js +12 -2
- package/lib/components/navigation/src/Navigation.js +15 -2
- package/lib/components/navigation/src/Navigation.stories.js +20 -4
- package/lib/components/selfie/src/SelfieCaptureScreens.js +3 -7
- package/lib/components/selfie/src/SelfieCaptureScreens.stories.js +16 -4
- package/lib/components/selfie/src/selfie-capture/SelfieCapture.js +23 -16
- package/lib/components/selfie/src/selfie-capture/SelfieCapture.stories.js +19 -7
- package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +16 -11
- package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.stories.js +14 -5
- package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +10 -5
- package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.stories.js +14 -5
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.stories.js +47 -0
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +5 -2
- package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +4 -4
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +6 -5
- package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +11 -9
- package/lib/components/selfie/src/smartselfie-capture/utils/imageCapture.ts +3 -1
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/totp-consent/src/TotpConsent.js +8 -3
- package/lib/domain/camera/src/SmartCamera.js +7 -22
- package/lib/domain/constants/src/Constants.js +28 -0
- package/lib/domain/file-upload/src/SmartFileUpload.js +9 -10
- package/lib/domain/localisation/index.js +456 -0
- package/package.json +12 -6
- package/dist/esm/DocumentCaptureScreens-C5BhNB-0.js.map +0 -1
- package/dist/esm/EndUserConsent-D4fd1ovG.js.map +0 -1
- package/dist/esm/Navigation-CTjK6tLU.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-KoQpCxtc.js.map +0 -1
- package/dist/esm/TotpConsent-CQU5jQi4.js.map +0 -1
- package/dist/esm/package-B-UwEdv7.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import validate from 'validate.js';
|
|
2
|
+
import { getDirection } from '../../../domain/localisation';
|
|
2
3
|
|
|
3
4
|
function postData(url, data) {
|
|
4
5
|
return fetch(url, {
|
|
@@ -284,7 +285,7 @@ function markup() {
|
|
|
284
285
|
}
|
|
285
286
|
</style>
|
|
286
287
|
|
|
287
|
-
<div class='flow center' id='id-entry'>
|
|
288
|
+
<div class='flow center' id='id-entry' dir='${this.direction}'>
|
|
288
289
|
<div class="nav">
|
|
289
290
|
<div class="back-wrapper">
|
|
290
291
|
<button type='button' data-type='icon' id="back-button" class="back-button">
|
|
@@ -331,7 +332,7 @@ function markup() {
|
|
|
331
332
|
</form>
|
|
332
333
|
</div>
|
|
333
334
|
|
|
334
|
-
<div hidden class='flow center' id='select-mode'>
|
|
335
|
+
<div hidden class='flow center' id='select-mode' dir='${this.direction}'>
|
|
335
336
|
<div class="nav">
|
|
336
337
|
<div class="back-wrapper">
|
|
337
338
|
<button type='button' data-type='icon' id="back-to-entry-button" class="back-button">
|
|
@@ -459,7 +460,7 @@ function markup() {
|
|
|
459
460
|
</form>
|
|
460
461
|
</div>
|
|
461
462
|
|
|
462
|
-
<div hidden class='flow center' id='otp-verification'>
|
|
463
|
+
<div hidden class='flow center' id='otp-verification' dir='${this.direction}'>
|
|
463
464
|
<div class="nav justify-right">
|
|
464
465
|
<button data-type='icon' type='button' class='close-iframe'>
|
|
465
466
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
|
|
@@ -899,6 +900,10 @@ class TotpConsent extends HTMLElement {
|
|
|
899
900
|
return this.getAttribute('theme-color') || '#001096';
|
|
900
901
|
}
|
|
901
902
|
|
|
903
|
+
get direction() {
|
|
904
|
+
return this.getAttribute('dir') || getDirection() || 'ltr';
|
|
905
|
+
}
|
|
906
|
+
|
|
902
907
|
get hideBack() {
|
|
903
908
|
return this.hasAttribute('hide-back');
|
|
904
909
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from '../../localisation';
|
|
2
|
+
|
|
1
3
|
class SmartCamera {
|
|
2
4
|
static stream = null;
|
|
3
5
|
|
|
@@ -74,32 +76,15 @@ class SmartCamera {
|
|
|
74
76
|
switch (e.name) {
|
|
75
77
|
case 'NotAllowedError':
|
|
76
78
|
case 'SecurityError':
|
|
77
|
-
return
|
|
78
|
-
Looks like camera access was not granted, or was blocked by a browser
|
|
79
|
-
level setting / extension. Please follow the prompt from the URL bar,
|
|
80
|
-
or extensions, and enable access.
|
|
81
|
-
You may need to refresh to start all over again
|
|
82
|
-
`;
|
|
79
|
+
return t('camera.error.notAllowed');
|
|
83
80
|
case 'AbortError':
|
|
84
|
-
return
|
|
85
|
-
Oops! Something happened, and we lost access to your stream.
|
|
86
|
-
Please refresh to start all over again
|
|
87
|
-
`;
|
|
81
|
+
return t('camera.error.abort');
|
|
88
82
|
case 'NotReadableError':
|
|
89
|
-
return
|
|
90
|
-
There seems to be a problem with your device's camera, or its connection.
|
|
91
|
-
Please check this, and when resolved, try again. Or try another device.
|
|
92
|
-
`;
|
|
83
|
+
return t('camera.error.notReadable');
|
|
93
84
|
case 'NotFoundError':
|
|
94
|
-
return
|
|
95
|
-
We are unable to find a video stream.
|
|
96
|
-
You may need to refresh to start all over again
|
|
97
|
-
`;
|
|
85
|
+
return t('camera.error.notFound');
|
|
98
86
|
case 'TypeError':
|
|
99
|
-
return
|
|
100
|
-
This site is insecure, and as such cannot have access to your camera.
|
|
101
|
-
Try to navigate to a secure version of this page, or contact the owner.
|
|
102
|
-
`;
|
|
87
|
+
return t('camera.error.insecure');
|
|
103
88
|
default:
|
|
104
89
|
return e.message;
|
|
105
90
|
}
|
|
@@ -23,5 +23,33 @@ export const IMAGE_TYPE = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export const DEFAULT_NO_OF_LIVENESS_FRAMES = 8;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JPEG compression quality for captured images (selfies, liveness frames, ID documents).
|
|
29
|
+
*
|
|
30
|
+
* Value: 0.92 (92% quality)
|
|
31
|
+
*
|
|
32
|
+
* This value is optimized for identity verification and biometric matching:
|
|
33
|
+
* - Preserves fine facial details needed for accurate facial recognition
|
|
34
|
+
* - Maintains skin tone gradients without JPEG blocking artifacts
|
|
35
|
+
* - Ensures ID document text remains readable for OCR processing
|
|
36
|
+
* - Provides minimal compression artifacts that could affect liveness detection
|
|
37
|
+
*
|
|
38
|
+
* Quality comparison:
|
|
39
|
+
* - 1.0: No compression, very large files
|
|
40
|
+
* - 0.95: Virtually lossless, larger files
|
|
41
|
+
* - 0.92: Good quality, reasonable file size
|
|
42
|
+
* - 0.85: Visible artifacts in gradients and skin tones
|
|
43
|
+
* - 0.80: Noticeable blocking artifacts that hurt matching accuracy
|
|
44
|
+
*
|
|
45
|
+
* WARNING: DO NOT LOWER THIS VALUE BELOW 0.92
|
|
46
|
+
* Reducing JPEG quality can negatively impact downstream systems:
|
|
47
|
+
* - Facial recognition matching accuracy may decrease
|
|
48
|
+
* - Liveness detection anti-spoofing checks may fail
|
|
49
|
+
* - ID document OCR text extraction may produce errors
|
|
50
|
+
* - Overall verification success rates may drop
|
|
51
|
+
*
|
|
52
|
+
*/
|
|
53
|
+
export const JPEG_QUALITY = 0.92;
|
|
26
54
|
export const PORTRAIT_ID_PREVIEW_WIDTH = 396;
|
|
27
55
|
export const PORTRAIT_ID_PREVIEW_HEIGHT = 527;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t, tHtml } from '../../localisation';
|
|
2
|
+
|
|
1
3
|
class SmartFileUpload {
|
|
2
4
|
static memoryLimit = 10240000;
|
|
3
5
|
|
|
@@ -27,11 +29,7 @@ class SmartFileUpload {
|
|
|
27
29
|
resolve(e.target.result);
|
|
28
30
|
};
|
|
29
31
|
reader.onerror = () => {
|
|
30
|
-
reject(
|
|
31
|
-
new Error(
|
|
32
|
-
'An error occurred reading the file. Please check the file, and try again',
|
|
33
|
-
),
|
|
34
|
-
);
|
|
32
|
+
reject(new Error(t('fileUpload.error.readingFile')));
|
|
35
33
|
};
|
|
36
34
|
reader.readAsDataURL(file);
|
|
37
35
|
});
|
|
@@ -39,20 +37,21 @@ class SmartFileUpload {
|
|
|
39
37
|
|
|
40
38
|
static async retrieve(files) {
|
|
41
39
|
if (files.length > 1) {
|
|
42
|
-
throw new Error('
|
|
40
|
+
throw new Error(t('fileUpload.error.multipleFiles'));
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
const file = files[0];
|
|
46
44
|
|
|
47
45
|
if (!SmartFileUpload.supportedTypes.includes(file.type)) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
'Unsupported file format. Please ensure that you are providing a JPG or PNG image',
|
|
50
|
-
);
|
|
46
|
+
throw new Error(t('fileUpload.error.unsupportedFormat'));
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
if (file.size > SmartFileUpload.memoryLimit) {
|
|
54
50
|
throw new Error(
|
|
55
|
-
|
|
51
|
+
tHtml('fileUpload.error.fileTooLarge', {
|
|
52
|
+
filename: file.name,
|
|
53
|
+
size: SmartFileUpload.getHumanSize(SmartFileUpload.memoryLimit),
|
|
54
|
+
}),
|
|
56
55
|
);
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal runtime i18n module for SmileID web components.
|
|
3
|
+
* Provides simple locale registration, loading, and translation lookup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import merge from 'lodash/merge';
|
|
7
|
+
|
|
8
|
+
// Bundle supported locales for offline/instant switching
|
|
9
|
+
import arLocale from '../../../locales/ar-EG.json';
|
|
10
|
+
import enLocale from '../../../locales/en-GB.json';
|
|
11
|
+
import frLocale from '../../../locales/fr-FR.json';
|
|
12
|
+
|
|
13
|
+
// Locale alias mapping for short codes
|
|
14
|
+
const LOCALE_ALIASES = {
|
|
15
|
+
ar: 'ar-EG',
|
|
16
|
+
en: 'en-GB',
|
|
17
|
+
fr: 'fr-FR',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve locale alias to full locale code.
|
|
22
|
+
* @param {string} lang - Language code (e.g., 'en', 'ar', 'en-GB')
|
|
23
|
+
* @returns {string} Resolved locale code
|
|
24
|
+
*/
|
|
25
|
+
function resolveLocale(lang) {
|
|
26
|
+
return LOCALE_ALIASES[lang] || lang;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_LOCALE = 'en-GB';
|
|
30
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
31
|
+
|
|
32
|
+
let currentLocale = DEFAULT_LOCALE;
|
|
33
|
+
const locales = {
|
|
34
|
+
'ar-EG': arLocale,
|
|
35
|
+
'en-GB': enLocale,
|
|
36
|
+
'fr-FR': frLocale,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register a locale object (in-memory).
|
|
41
|
+
* @param {string} lang - Language code (e.g., 'en-GB', 'ar-EG')
|
|
42
|
+
* @param {object} data - Locale translation object
|
|
43
|
+
*/
|
|
44
|
+
export function registerLocale(lang, data) {
|
|
45
|
+
locales[lang] = data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deep merge source object into target object.
|
|
50
|
+
* Recursively merges nested objects while preserving non-overridden values.
|
|
51
|
+
* Uses lodash merge under the hood but returns a new object (does not mutate inputs).
|
|
52
|
+
* @param {object} target - Base object to merge into
|
|
53
|
+
* @param {object} source - Object with values to merge/override
|
|
54
|
+
* @returns {object} New merged object (does not mutate inputs)
|
|
55
|
+
*/
|
|
56
|
+
export function deepMerge(target, source) {
|
|
57
|
+
if (!source || typeof source !== 'object') {
|
|
58
|
+
return target;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!target || typeof target !== 'object') {
|
|
62
|
+
return source;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Use lodash merge with empty object as first arg to avoid mutating target
|
|
66
|
+
return merge({}, target, source);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Required translation keys for a complete locale.
|
|
71
|
+
* These are the minimum keys needed for the SDK to function properly.
|
|
72
|
+
*/
|
|
73
|
+
const REQUIRED_LOCALE_KEYS = [
|
|
74
|
+
'direction',
|
|
75
|
+
'common.back',
|
|
76
|
+
'common.close',
|
|
77
|
+
'common.continue',
|
|
78
|
+
'common.cancel',
|
|
79
|
+
'camera.permission.description',
|
|
80
|
+
'camera.permission.requestButton',
|
|
81
|
+
'camera.error.notAllowed',
|
|
82
|
+
'selfie.instructions.title',
|
|
83
|
+
'selfie.capture.button.takeSelfie',
|
|
84
|
+
'selfie.review.title',
|
|
85
|
+
'selfie.review.acceptButton',
|
|
86
|
+
'selfie.review.retakeButton',
|
|
87
|
+
'document.capture.captureButton',
|
|
88
|
+
'document.review.acceptButton',
|
|
89
|
+
'document.review.retakeButton',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fetch with timeout wrapper.
|
|
94
|
+
* @param {string} url - URL to fetch
|
|
95
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
96
|
+
* @returns {Promise<Response>} Fetch response
|
|
97
|
+
*/
|
|
98
|
+
async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT_MS) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
104
|
+
return response;
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a locale by URL (fetch and cache).
|
|
112
|
+
* @param {string} lang - Language code
|
|
113
|
+
* @param {string} url - URL to locale JSON file
|
|
114
|
+
* @returns {Promise<object>} Loaded locale data
|
|
115
|
+
*/
|
|
116
|
+
export async function registerLocaleUrl(lang, url) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetchWithTimeout(url);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`Failed to load locale from ${url}: ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
registerLocale(lang, data);
|
|
124
|
+
return data;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const errorMessage =
|
|
127
|
+
error.name === 'AbortError'
|
|
128
|
+
? `Timeout loading locale '${lang}' from ${url}`
|
|
129
|
+
: `Error loading locale '${lang}' from ${url}: ${error.message}`;
|
|
130
|
+
console.error(errorMessage);
|
|
131
|
+
|
|
132
|
+
// Fallback to default locale if available
|
|
133
|
+
if (lang !== DEFAULT_LOCALE && locales[DEFAULT_LOCALE]) {
|
|
134
|
+
console.warn(`Falling back to default locale '${DEFAULT_LOCALE}'`);
|
|
135
|
+
return locales[DEFAULT_LOCALE];
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load a locale (supports both inline registration and URL fetch).
|
|
143
|
+
* @param {string} lang - Language code
|
|
144
|
+
* @param {string} [url] - Optional URL to fetch locale from
|
|
145
|
+
* @returns {Promise<object>} Loaded locale data
|
|
146
|
+
*/
|
|
147
|
+
export async function loadLocale(lang, url) {
|
|
148
|
+
const resolvedLang = resolveLocale(lang);
|
|
149
|
+
|
|
150
|
+
// Return cached locale if available
|
|
151
|
+
if (locales[resolvedLang]) {
|
|
152
|
+
return locales[resolvedLang];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fetch from URL if provided
|
|
156
|
+
if (url) {
|
|
157
|
+
return registerLocaleUrl(lang, url);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// No URL provided and locale not cached - fallback to default
|
|
161
|
+
console.warn(
|
|
162
|
+
`Locale '${lang}' not found and no URL provided, using default '${DEFAULT_LOCALE}'`,
|
|
163
|
+
);
|
|
164
|
+
return locales[DEFAULT_LOCALE] || {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Helper to get nested value from object using dot notation.
|
|
169
|
+
* @param {object} obj - Object to traverse
|
|
170
|
+
* @param {string} key - Dot-separated key path
|
|
171
|
+
* @returns {string|undefined} Value at path or undefined
|
|
172
|
+
*/
|
|
173
|
+
function getNestedValue(obj, key) {
|
|
174
|
+
if (!obj) return undefined;
|
|
175
|
+
|
|
176
|
+
const value = key.split('.').reduce((acc, k) => {
|
|
177
|
+
if (acc && typeof acc === 'object' && k in acc) {
|
|
178
|
+
return acc[k];
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}, obj);
|
|
182
|
+
|
|
183
|
+
return typeof value === 'string' ? value : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validate that a locale has all required translation keys.
|
|
188
|
+
* @param {object} locale - Locale data to validate
|
|
189
|
+
* @returns {{ missingKeys: string[], valid: boolean }} Validation result
|
|
190
|
+
*/
|
|
191
|
+
export function validateLocale(locale) {
|
|
192
|
+
if (!locale || typeof locale !== 'object') {
|
|
193
|
+
return { missingKeys: REQUIRED_LOCALE_KEYS, valid: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const missingKeys = REQUIRED_LOCALE_KEYS.filter((key) => {
|
|
197
|
+
if (key === 'direction') {
|
|
198
|
+
return !locale.direction;
|
|
199
|
+
}
|
|
200
|
+
return !getNestedValue(locale, key);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
missingKeys,
|
|
205
|
+
valid: missingKeys.length === 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get translation for a key.
|
|
211
|
+
* Supports nested keys with dot notation (e.g., 'camera.permission.description').
|
|
212
|
+
* Fallback chain: current locale → default locale (English) → raw key.
|
|
213
|
+
* @param {string} key - Translation key
|
|
214
|
+
* @returns {string} Translated string or fallback
|
|
215
|
+
*/
|
|
216
|
+
export function t(key) {
|
|
217
|
+
// Try current locale first
|
|
218
|
+
const resolvedLocale = resolveLocale(currentLocale);
|
|
219
|
+
const currentLocaleData = locales[resolvedLocale];
|
|
220
|
+
const value = getNestedValue(currentLocaleData, key);
|
|
221
|
+
if (value) {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback to default locale if different from current
|
|
226
|
+
const resolvedDefault = resolveLocale(DEFAULT_LOCALE);
|
|
227
|
+
if (resolvedLocale !== resolvedDefault) {
|
|
228
|
+
const defaultValue = getNestedValue(locales[resolvedDefault], key);
|
|
229
|
+
if (defaultValue) {
|
|
230
|
+
return defaultValue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Final fallback: return the key itself
|
|
235
|
+
console.warn(`Translation key '${key}' not found in any locale`);
|
|
236
|
+
return key;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Alias for t() function.
|
|
241
|
+
* @see t
|
|
242
|
+
*/
|
|
243
|
+
export const translate = t;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* HTML entity map for escaping special characters.
|
|
247
|
+
*/
|
|
248
|
+
const HTML_ENTITIES = {
|
|
249
|
+
'"': '"',
|
|
250
|
+
'&': '&',
|
|
251
|
+
"'": ''',
|
|
252
|
+
'<': '<',
|
|
253
|
+
'>': '>',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Escape HTML special characters to prevent XSS.
|
|
258
|
+
* @param {string} str - String to escape
|
|
259
|
+
* @returns {string} Escaped string
|
|
260
|
+
*/
|
|
261
|
+
export function escapeHtml(str) {
|
|
262
|
+
return str.replace(/[&<>"']/g, (char) => HTML_ENTITIES[char]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get translation with HTML interpolation for styled placeholders.
|
|
267
|
+
* Placeholders in format {{key}} will be replaced with provided values.
|
|
268
|
+
* Values can be plain strings or objects with {value, className} for styled spans.
|
|
269
|
+
* @param {string} key - Translation key
|
|
270
|
+
* @param {Object} params - Interpolation parameters
|
|
271
|
+
* @returns {string} Translated string with interpolations
|
|
272
|
+
* @example
|
|
273
|
+
* // Plain interpolation
|
|
274
|
+
* tHtml('greeting', { name: 'John' }) // "Hello, John"
|
|
275
|
+
*
|
|
276
|
+
* // Styled interpolation
|
|
277
|
+
* tHtml('consent.accessRequest', {
|
|
278
|
+
* partnerName: { value: 'Acme', className: 'theme' }
|
|
279
|
+
* }) // "<span class="theme">Acme</span> wants to access..."
|
|
280
|
+
*/
|
|
281
|
+
export function tHtml(key, params = {}) {
|
|
282
|
+
let text = t(key);
|
|
283
|
+
|
|
284
|
+
Object.keys(params).forEach((paramKey) => {
|
|
285
|
+
const paramValue = params[paramKey];
|
|
286
|
+
const placeholder = `{{${paramKey}}}`;
|
|
287
|
+
|
|
288
|
+
if (paramValue && typeof paramValue === 'object' && 'value' in paramValue) {
|
|
289
|
+
// Styled interpolation: { value: 'text', className: 'theme' }
|
|
290
|
+
const escapedValue = escapeHtml(String(paramValue.value || ''));
|
|
291
|
+
const className = escapeHtml(String(paramValue.className || ''));
|
|
292
|
+
text = text
|
|
293
|
+
.split(placeholder)
|
|
294
|
+
.join(
|
|
295
|
+
className
|
|
296
|
+
? `<span class="${className}">${escapedValue}</span>`
|
|
297
|
+
: escapedValue,
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
// Plain text interpolation
|
|
301
|
+
text = text.split(placeholder).join(escapeHtml(String(paramValue)));
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return text;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Alias for tHtml() function.
|
|
310
|
+
* @see tHtml
|
|
311
|
+
*/
|
|
312
|
+
export const translateHtml = tHtml;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Set the current locale.
|
|
316
|
+
* If the locale is not registered, it will attempt to load it from the provided URL.
|
|
317
|
+
* Applies RTL direction to document element if direction is specified in locale.
|
|
318
|
+
* @param {string} lang - Language code
|
|
319
|
+
* @param {Object} [options] - Configuration options
|
|
320
|
+
* @param {string} [options.url] - URL to fetch locale from if not registered
|
|
321
|
+
* @param {Object} [options.translation] - Complete locale data object (legacy)
|
|
322
|
+
* @param {Object} [options.locales] - Locale data keyed by language code (new API)
|
|
323
|
+
* @param {boolean} [options.validate] - Whether to validate locale completeness
|
|
324
|
+
* @returns {Promise<boolean>} Whether locale was successfully set
|
|
325
|
+
*/
|
|
326
|
+
export async function setCurrentLocale(
|
|
327
|
+
lang,
|
|
328
|
+
{ url, translation, locales: customLocales, validate = false } = {},
|
|
329
|
+
) {
|
|
330
|
+
currentLocale = resolveLocale(lang);
|
|
331
|
+
// Step 1: Process custom locales (new API - keyed by language code)
|
|
332
|
+
if (customLocales && typeof customLocales === 'object') {
|
|
333
|
+
Object.entries(customLocales).forEach(([localeKey, localeData]) => {
|
|
334
|
+
if (!localeData || typeof localeData !== 'object') {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const resolvedKey = resolveLocale(localeKey);
|
|
339
|
+
|
|
340
|
+
if (locales[resolvedKey]) {
|
|
341
|
+
// Deep merge into existing bundled locale
|
|
342
|
+
locales[resolvedKey] = deepMerge(locales[resolvedKey], localeData);
|
|
343
|
+
} else {
|
|
344
|
+
// Register as new locale
|
|
345
|
+
registerLocale(resolvedKey, localeData);
|
|
346
|
+
|
|
347
|
+
// Add alias if short code provided
|
|
348
|
+
if (localeKey !== resolvedKey) {
|
|
349
|
+
LOCALE_ALIASES[localeKey] = resolvedKey;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Step 2: Handle legacy translation option (for backward compatibility)
|
|
356
|
+
if (!locales[currentLocale]) {
|
|
357
|
+
if (translation) {
|
|
358
|
+
registerLocale(currentLocale, translation);
|
|
359
|
+
} else if (url) {
|
|
360
|
+
try {
|
|
361
|
+
await loadLocale(currentLocale, url);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error(
|
|
364
|
+
`Failed to load locale '${lang}', keeping current locale '${currentLocale}'`,
|
|
365
|
+
);
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
console.warn(`Locale '${lang}' not registered and no URL provided`);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Step 3: Validate locale completeness if requested
|
|
375
|
+
if (validate && locales[currentLocale]) {
|
|
376
|
+
const validation = validateLocale(locales[currentLocale]);
|
|
377
|
+
if (!validation.valid) {
|
|
378
|
+
console.warn(
|
|
379
|
+
`Locale '${lang}' is missing required keys:`,
|
|
380
|
+
validation.missingKeys,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Step 4: Apply RTL/LTR direction if specified in locale data
|
|
386
|
+
const locale = locales[currentLocale];
|
|
387
|
+
if (locale && locale.direction && document?.documentElement) {
|
|
388
|
+
document.documentElement.dir = locale.direction;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get the current locale.
|
|
396
|
+
* @returns {string} Current language code
|
|
397
|
+
*/
|
|
398
|
+
export function getCurrentLocale() {
|
|
399
|
+
return currentLocale;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get the default locale code.
|
|
404
|
+
* @returns {string} Default language code
|
|
405
|
+
*/
|
|
406
|
+
export function getDefaultLocale() {
|
|
407
|
+
return DEFAULT_LOCALE;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if a locale is registered.
|
|
412
|
+
* @param {string} lang - Language code
|
|
413
|
+
* @returns {boolean} Whether locale is registered
|
|
414
|
+
*/
|
|
415
|
+
export function hasLocale(lang) {
|
|
416
|
+
return lang in locales;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Set document direction based on locale direction property.
|
|
421
|
+
* @param {string} lang - Language code
|
|
422
|
+
*/
|
|
423
|
+
export function setDocumentDir(lang) {
|
|
424
|
+
const locale = locales[lang];
|
|
425
|
+
if (locale && locale.direction) {
|
|
426
|
+
document.documentElement.dir = locale.direction;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get the text direction for the current locale.
|
|
432
|
+
* @returns {string} Direction ('ltr' or 'rtl'), defaults to 'ltr'
|
|
433
|
+
*/
|
|
434
|
+
export function getDirection() {
|
|
435
|
+
const locale = locales[currentLocale];
|
|
436
|
+
return locale?.direction || 'ltr';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export default {
|
|
440
|
+
deepMerge,
|
|
441
|
+
escapeHtml,
|
|
442
|
+
getCurrentLocale,
|
|
443
|
+
getDefaultLocale,
|
|
444
|
+
getDirection,
|
|
445
|
+
hasLocale,
|
|
446
|
+
loadLocale,
|
|
447
|
+
registerLocale,
|
|
448
|
+
registerLocaleUrl,
|
|
449
|
+
setCurrentLocale,
|
|
450
|
+
setDocumentDir,
|
|
451
|
+
t,
|
|
452
|
+
tHtml,
|
|
453
|
+
translate,
|
|
454
|
+
translateHtml,
|
|
455
|
+
validateLocale,
|
|
456
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/web-components",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"main": "dist/esm/main.js",
|
|
5
5
|
"module": "dist/esm/main.js",
|
|
6
6
|
"types": "dist/types/main.d.ts",
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"./smart-camera-web": {
|
|
41
41
|
"types": "./dist/types/smart-camera-web.d.ts",
|
|
42
42
|
"import": "./dist/esm/smart-camera-web.js"
|
|
43
|
+
},
|
|
44
|
+
"./localisation": {
|
|
45
|
+
"types": "./dist/types/localisation.d.ts",
|
|
46
|
+
"import": "./dist/esm/localisation.js"
|
|
43
47
|
}
|
|
44
48
|
},
|
|
45
49
|
"files": [
|
|
@@ -76,7 +80,7 @@
|
|
|
76
80
|
"@preact/signals": "^2.1.1",
|
|
77
81
|
"@tabler/icons-preact": "^3.34.0",
|
|
78
82
|
"lodash": "^4.17.21",
|
|
79
|
-
"preact": "^10.
|
|
83
|
+
"preact": "^10.27.3",
|
|
80
84
|
"preact-custom-element": "^4.3.0",
|
|
81
85
|
"preact-router": "^4.1.2",
|
|
82
86
|
"signature_pad": "^5.0.2",
|
|
@@ -87,25 +91,27 @@
|
|
|
87
91
|
"@types/lodash": "^4.17.20",
|
|
88
92
|
"@types/node": "^20.11.24",
|
|
89
93
|
"@types/preact-custom-element": "^4.0.4",
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
91
|
-
"@typescript-eslint/parser": "^
|
|
94
|
+
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
|
95
|
+
"@typescript-eslint/parser": "^8.49.0",
|
|
92
96
|
"cross-env": "^7.0.3",
|
|
93
97
|
"cypress": "^13.15.0",
|
|
94
98
|
"eslint": "^8.57.0",
|
|
95
99
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
96
|
-
"eslint-config-airbnb-typescript": "^18.0.0",
|
|
97
100
|
"eslint-config-prettier": "^9.1.0",
|
|
98
101
|
"eslint-import-resolver-typescript": "^3.6.1",
|
|
99
102
|
"eslint-plugin-cypress": "^3.3.0",
|
|
100
103
|
"eslint-plugin-import": "^2.29.1",
|
|
101
104
|
"eslint-plugin-jest": "^28.8.3",
|
|
102
105
|
"eslint-plugin-prettier": "^5.2.1",
|
|
103
|
-
"glob": "^10.
|
|
106
|
+
"glob": "^10.5.0",
|
|
104
107
|
"prettier": "^3.6.2",
|
|
105
108
|
"rollup-plugin-visualizer": "^6.0.3",
|
|
106
109
|
"typescript": "^5.8.3",
|
|
107
110
|
"vite": "^7.2.2",
|
|
108
111
|
"vite-plugin-dts": "^4.5.4",
|
|
109
112
|
"vite-plugin-tsconfig-paths": "^1.4.1"
|
|
113
|
+
},
|
|
114
|
+
"optionalDependencies": {
|
|
115
|
+
"@rollup/rollup-linux-x64-gnu": "^4.53.5"
|
|
110
116
|
}
|
|
111
117
|
}
|