@naturalcycles/js-lib 14.255.0 → 14.257.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/cfg/frontend/tsconfig.json +67 -0
- package/dist/bot.d.ts +60 -0
- package/dist/bot.js +129 -0
- package/dist/browser/adminService.d.ts +69 -0
- package/dist/browser/adminService.js +98 -0
- package/dist/browser/analytics.util.d.ts +12 -0
- package/dist/browser/analytics.util.js +59 -0
- package/dist/browser/i18n/fetchTranslationLoader.d.ts +13 -0
- package/dist/browser/i18n/fetchTranslationLoader.js +17 -0
- package/dist/browser/i18n/translation.service.d.ts +53 -0
- package/dist/browser/i18n/translation.service.js +61 -0
- package/dist/browser/imageFitter.d.ts +60 -0
- package/dist/browser/imageFitter.js +69 -0
- package/dist/browser/script.util.d.ts +14 -0
- package/dist/browser/script.util.js +50 -0
- package/dist/browser/topbar.d.ts +23 -0
- package/dist/browser/topbar.js +137 -0
- package/dist/decorators/memo.util.d.ts +2 -1
- package/dist/decorators/memo.util.js +8 -6
- package/dist/decorators/swarmSafe.decorator.d.ts +9 -0
- package/dist/decorators/swarmSafe.decorator.js +42 -0
- package/dist/error/assert.d.ts +2 -1
- package/dist/error/assert.js +15 -13
- package/dist/error/error.util.js +9 -6
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/zod/zod.util.d.ts +1 -1
- package/dist-esm/bot.js +125 -0
- package/dist-esm/browser/adminService.js +94 -0
- package/dist-esm/browser/analytics.util.js +54 -0
- package/dist-esm/browser/i18n/fetchTranslationLoader.js +13 -0
- package/dist-esm/browser/i18n/translation.service.js +56 -0
- package/dist-esm/browser/imageFitter.js +65 -0
- package/dist-esm/browser/script.util.js +46 -0
- package/dist-esm/browser/topbar.js +134 -0
- package/dist-esm/decorators/memo.util.js +3 -1
- package/dist-esm/decorators/swarmSafe.decorator.js +38 -0
- package/dist-esm/error/assert.js +3 -1
- package/dist-esm/error/error.util.js +4 -1
- package/dist-esm/index.js +8 -0
- package/package.json +2 -1
- package/src/bot.ts +155 -0
- package/src/browser/adminService.ts +157 -0
- package/src/browser/analytics.util.ts +68 -0
- package/src/browser/i18n/fetchTranslationLoader.ts +16 -0
- package/src/browser/i18n/translation.service.ts +102 -0
- package/src/browser/imageFitter.ts +128 -0
- package/src/browser/script.util.ts +52 -0
- package/src/browser/topbar.ts +147 -0
- package/src/datetime/localDate.ts +16 -0
- package/src/datetime/localTime.ts +39 -0
- package/src/decorators/debounce.ts +1 -0
- package/src/decorators/memo.util.ts +4 -1
- package/src/decorators/swarmSafe.decorator.ts +47 -0
- package/src/error/assert.ts +5 -11
- package/src/error/error.util.ts +4 -1
- package/src/index.ts +8 -0
- package/src/json-schema/jsonSchemaBuilder.ts +20 -0
- package/src/semver.ts +2 -0
- package/src/zod/zod.util.ts +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// @naturalcycles/js-lib/cfg/frontend/tsconfig.json
|
|
3
|
+
//
|
|
4
|
+
// Shared tsconfig for Frontend applications
|
|
5
|
+
//
|
|
6
|
+
{
|
|
7
|
+
"compilerOptions": {
|
|
8
|
+
// Target/module
|
|
9
|
+
"target": "es2020", // es2020+ browsers, adjust to your requirements!
|
|
10
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
// specifying these explicitly for better IDE compatibility (but they're on by default with module=nodenext)
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
// Faster compilation in general
|
|
18
|
+
// Support for external compilers (e.g esbuild)
|
|
19
|
+
// Speedup in Jest by using "isolatedModules" in 'ts-jest' config
|
|
20
|
+
"isolatedModules": true,
|
|
21
|
+
|
|
22
|
+
// Emit
|
|
23
|
+
"sourceMap": false,
|
|
24
|
+
"declaration": false,
|
|
25
|
+
// Otherwise since es2022 it defaults to true
|
|
26
|
+
// and starts to produce different/unexpected behavior
|
|
27
|
+
// https://angular.schule/blog/2022-11-use-define-for-class-fields
|
|
28
|
+
"useDefineForClassFields": false,
|
|
29
|
+
"importHelpers": true,
|
|
30
|
+
|
|
31
|
+
// Strictness
|
|
32
|
+
"strict": true,
|
|
33
|
+
"noFallthroughCasesInSwitch": true,
|
|
34
|
+
"forceConsistentCasingInFileNames": true,
|
|
35
|
+
"resolveJsonModule": true,
|
|
36
|
+
"suppressImplicitAnyIndexErrors": false,
|
|
37
|
+
"noUncheckedIndexedAccess": true,
|
|
38
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
39
|
+
"noImplicitOverride": true,
|
|
40
|
+
|
|
41
|
+
// Enabled should be faster, but will catch less errors
|
|
42
|
+
// "skipLibCheck": true,
|
|
43
|
+
|
|
44
|
+
// Disabled because of https://github.com/Microsoft/TypeScript/issues/29172
|
|
45
|
+
// Need to be specified in the project tsconfig
|
|
46
|
+
// "outDir": "dist",
|
|
47
|
+
// "rootDir": "./src",
|
|
48
|
+
// "baseUrl": "./",
|
|
49
|
+
// "paths": {
|
|
50
|
+
// "@src/*": ["src/*"]
|
|
51
|
+
// },
|
|
52
|
+
// "typeRoots": [
|
|
53
|
+
// "node_modules/@types",
|
|
54
|
+
// "src/@types"
|
|
55
|
+
// ],
|
|
56
|
+
|
|
57
|
+
// Other
|
|
58
|
+
"jsx": "preserve",
|
|
59
|
+
"pretty": true,
|
|
60
|
+
"newLine": "lf",
|
|
61
|
+
"experimentalDecorators": true,
|
|
62
|
+
// "emitDecoratorMetadata": true // use if needed
|
|
63
|
+
},
|
|
64
|
+
// Need to be specified in the project tsconfig
|
|
65
|
+
// "include": ["src"],
|
|
66
|
+
// "exclude": ["**/__exclude", "**/@linked"]
|
|
67
|
+
}
|
package/dist/bot.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface BotDetectionServiceCfg {
|
|
2
|
+
/**
|
|
3
|
+
* Defaults to false.
|
|
4
|
+
* If true - the instance will memoize (remember) the results of the detection
|
|
5
|
+
* and won't re-run it.
|
|
6
|
+
*/
|
|
7
|
+
memoizeResults?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Defaults to false.
|
|
10
|
+
* If set to true: `getBotReason()` would return BotReason.CDP if CDP is detected.
|
|
11
|
+
* Otherwise - `getBotReason()` will not perform the CDP check.
|
|
12
|
+
*/
|
|
13
|
+
treatCDPAsBotReason?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Service to detect bots and CDP (Chrome DevTools Protocol).
|
|
17
|
+
*
|
|
18
|
+
* @experimental
|
|
19
|
+
*/
|
|
20
|
+
export declare class BotDetectionService {
|
|
21
|
+
cfg: BotDetectionServiceCfg;
|
|
22
|
+
constructor(cfg?: BotDetectionServiceCfg);
|
|
23
|
+
private botReason;
|
|
24
|
+
private cdp;
|
|
25
|
+
isBotOrCDP(): boolean;
|
|
26
|
+
isBot(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Returns null if it's not a Bot,
|
|
29
|
+
* otherwise a truthy BotReason.
|
|
30
|
+
*/
|
|
31
|
+
getBotReason(): BotReason | null;
|
|
32
|
+
private detectBotReason;
|
|
33
|
+
/**
|
|
34
|
+
* CDP stands for Chrome DevTools Protocol.
|
|
35
|
+
* This function tests if the current environment is a CDP environment.
|
|
36
|
+
* If it's true - it's one of:
|
|
37
|
+
*
|
|
38
|
+
* 1. Bot, automated with CDP, e.g Puppeteer, Playwright or such.
|
|
39
|
+
* 2. Developer with Chrome DevTools open.
|
|
40
|
+
*
|
|
41
|
+
* 2 is certainly not a bot, but unfortunately we can't distinguish between the two.
|
|
42
|
+
* That's why this function is not part of `isBot()`, because it can give "false positive" with DevTools.
|
|
43
|
+
*
|
|
44
|
+
* Based on: https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024
|
|
45
|
+
*/
|
|
46
|
+
isCDP(): boolean;
|
|
47
|
+
private detectCDP;
|
|
48
|
+
}
|
|
49
|
+
export declare enum BotReason {
|
|
50
|
+
NoNavigator = 1,
|
|
51
|
+
NoUserAgent = 2,
|
|
52
|
+
UserAgent = 3,
|
|
53
|
+
WebDriver = 4,
|
|
54
|
+
EmptyLanguages = 6,
|
|
55
|
+
/**
|
|
56
|
+
* This is when CDP is considered to be a reason to be a Bot.
|
|
57
|
+
* By default it's not.
|
|
58
|
+
*/
|
|
59
|
+
CDP = 8
|
|
60
|
+
}
|
package/dist/bot.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Relevant material:
|
|
3
|
+
// https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.BotReason = exports.BotDetectionService = void 0;
|
|
6
|
+
const env_1 = require("./env");
|
|
7
|
+
/**
|
|
8
|
+
* Service to detect bots and CDP (Chrome DevTools Protocol).
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
class BotDetectionService {
|
|
13
|
+
constructor(cfg = {}) {
|
|
14
|
+
this.cfg = cfg;
|
|
15
|
+
}
|
|
16
|
+
isBotOrCDP() {
|
|
17
|
+
return !!this.getBotReason() || this.isCDP();
|
|
18
|
+
}
|
|
19
|
+
isBot() {
|
|
20
|
+
return !!this.getBotReason();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns null if it's not a Bot,
|
|
24
|
+
* otherwise a truthy BotReason.
|
|
25
|
+
*/
|
|
26
|
+
getBotReason() {
|
|
27
|
+
if (this.cfg.memoizeResults && this.botReason !== undefined) {
|
|
28
|
+
return this.botReason;
|
|
29
|
+
}
|
|
30
|
+
this.botReason = this.detectBotReason();
|
|
31
|
+
return this.botReason;
|
|
32
|
+
}
|
|
33
|
+
detectBotReason() {
|
|
34
|
+
// SSR - not a bot
|
|
35
|
+
if ((0, env_1.isServerSide)())
|
|
36
|
+
return null;
|
|
37
|
+
const { navigator } = globalThis;
|
|
38
|
+
if (!navigator)
|
|
39
|
+
return BotReason.NoNavigator;
|
|
40
|
+
const { userAgent } = navigator;
|
|
41
|
+
if (!userAgent)
|
|
42
|
+
return BotReason.NoUserAgent;
|
|
43
|
+
if (/bot|headless|electron|phantom|slimer/i.test(userAgent)) {
|
|
44
|
+
return BotReason.UserAgent;
|
|
45
|
+
}
|
|
46
|
+
if (navigator.webdriver) {
|
|
47
|
+
return BotReason.WebDriver;
|
|
48
|
+
}
|
|
49
|
+
// Kirill: commented out, as it's no longer seems reliable,
|
|
50
|
+
// e.g generates false positives with latest Android clients (e.g. Chrome 129)
|
|
51
|
+
// if (navigator.plugins?.length === 0) {
|
|
52
|
+
// return BotReason.ZeroPlugins // Headless Chrome
|
|
53
|
+
// }
|
|
54
|
+
if (navigator.languages === '') {
|
|
55
|
+
return BotReason.EmptyLanguages; // Headless Chrome
|
|
56
|
+
}
|
|
57
|
+
// isChrome is true if the browser is Chrome, Chromium or Opera
|
|
58
|
+
// this is "the chrome test" from https://intoli.com/blog/not-possible-to-block-chrome-headless/
|
|
59
|
+
// this property is for some reason not present by default in headless chrome
|
|
60
|
+
// Kirill: criterium removed due to false positives with Android
|
|
61
|
+
// if (userAgent.includes('Chrome') && !(globalThis as any).chrome) {
|
|
62
|
+
// return BotReason.ChromeWithoutChrome // Headless Chrome
|
|
63
|
+
// }
|
|
64
|
+
if (this.cfg.treatCDPAsBotReason && this.detectCDP()) {
|
|
65
|
+
return BotReason.CDP;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* CDP stands for Chrome DevTools Protocol.
|
|
71
|
+
* This function tests if the current environment is a CDP environment.
|
|
72
|
+
* If it's true - it's one of:
|
|
73
|
+
*
|
|
74
|
+
* 1. Bot, automated with CDP, e.g Puppeteer, Playwright or such.
|
|
75
|
+
* 2. Developer with Chrome DevTools open.
|
|
76
|
+
*
|
|
77
|
+
* 2 is certainly not a bot, but unfortunately we can't distinguish between the two.
|
|
78
|
+
* That's why this function is not part of `isBot()`, because it can give "false positive" with DevTools.
|
|
79
|
+
*
|
|
80
|
+
* Based on: https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024
|
|
81
|
+
*/
|
|
82
|
+
isCDP() {
|
|
83
|
+
if (this.cfg.memoizeResults && this.cdp !== undefined) {
|
|
84
|
+
return this.cdp;
|
|
85
|
+
}
|
|
86
|
+
this.cdp = this.detectCDP();
|
|
87
|
+
return this.cdp;
|
|
88
|
+
}
|
|
89
|
+
detectCDP() {
|
|
90
|
+
if ((0, env_1.isServerSide)())
|
|
91
|
+
return false;
|
|
92
|
+
let cdpCheck1 = false;
|
|
93
|
+
try {
|
|
94
|
+
/* eslint-disable */
|
|
95
|
+
// biome-ignore lint/suspicious/useErrorMessage: ok
|
|
96
|
+
const e = new window.Error();
|
|
97
|
+
window.Object.defineProperty(e, 'stack', {
|
|
98
|
+
configurable: false,
|
|
99
|
+
enumerable: false,
|
|
100
|
+
// biome-ignore lint/complexity/useArrowFunction: ok
|
|
101
|
+
get: function () {
|
|
102
|
+
cdpCheck1 = true;
|
|
103
|
+
return '';
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
// This is part of the detection and shouldn't be deleted
|
|
107
|
+
window.console.debug(e);
|
|
108
|
+
/* eslint-enable */
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
return cdpCheck1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.BotDetectionService = BotDetectionService;
|
|
115
|
+
var BotReason;
|
|
116
|
+
(function (BotReason) {
|
|
117
|
+
BotReason[BotReason["NoNavigator"] = 1] = "NoNavigator";
|
|
118
|
+
BotReason[BotReason["NoUserAgent"] = 2] = "NoUserAgent";
|
|
119
|
+
BotReason[BotReason["UserAgent"] = 3] = "UserAgent";
|
|
120
|
+
BotReason[BotReason["WebDriver"] = 4] = "WebDriver";
|
|
121
|
+
// ZeroPlugins = 5,
|
|
122
|
+
BotReason[BotReason["EmptyLanguages"] = 6] = "EmptyLanguages";
|
|
123
|
+
// ChromeWithoutChrome = 7,
|
|
124
|
+
/**
|
|
125
|
+
* This is when CDP is considered to be a reason to be a Bot.
|
|
126
|
+
* By default it's not.
|
|
127
|
+
*/
|
|
128
|
+
BotReason[BotReason["CDP"] = 8] = "CDP";
|
|
129
|
+
})(BotReason || (exports.BotReason = BotReason = {}));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Promisable } from '../typeFest';
|
|
2
|
+
export interface AdminModeCfg {
|
|
3
|
+
/**
|
|
4
|
+
* Function (predicate) to detect if needed keys are pressed.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* predicate: e => e.ctrlKey && e.key === 'L'
|
|
8
|
+
*
|
|
9
|
+
* @default
|
|
10
|
+
* Detects Ctrl+Shift+L
|
|
11
|
+
*/
|
|
12
|
+
predicate?: (e: KeyboardEvent) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Called when RedDot is clicked. Implies that AdminMode is enabled.
|
|
15
|
+
*/
|
|
16
|
+
onRedDotClick?: () => any;
|
|
17
|
+
/**
|
|
18
|
+
* Called when AdminMode was changed.
|
|
19
|
+
*/
|
|
20
|
+
onChange?: (adminMode: boolean) => any;
|
|
21
|
+
/**
|
|
22
|
+
* Called BEFORE entering AdminMode.
|
|
23
|
+
* Serves as a predicate that can cancel entering AdminMode if false is returned.
|
|
24
|
+
* Return true to allow.
|
|
25
|
+
* Function is awaited before proceeding.
|
|
26
|
+
*/
|
|
27
|
+
beforeEnter?: () => Promisable<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Called BEFORE exiting AdminMode.
|
|
30
|
+
* Serves as a predicate that can cancel exiting AdminMode if false is returned.
|
|
31
|
+
* Return true to allow.
|
|
32
|
+
* Function is awaited before proceeding.
|
|
33
|
+
*/
|
|
34
|
+
beforeExit?: () => Promisable<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* @default true
|
|
37
|
+
* If true - it will "persist" the adminMode state in LocalStorage
|
|
38
|
+
*/
|
|
39
|
+
persistToLocalStorage?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The key for LocalStorage persistence.
|
|
42
|
+
*
|
|
43
|
+
* @default '__adminMode__'
|
|
44
|
+
*/
|
|
45
|
+
localStorageKey?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @experimental
|
|
49
|
+
*
|
|
50
|
+
* Allows to listen for AdminMode keypress combination (Ctrl+Shift+L by default) to toggle AdminMode,
|
|
51
|
+
* indicated by RedDot DOM element.
|
|
52
|
+
*
|
|
53
|
+
* todo: help with Authentication
|
|
54
|
+
*/
|
|
55
|
+
export declare class AdminService {
|
|
56
|
+
constructor(cfg?: AdminModeCfg);
|
|
57
|
+
cfg: Required<AdminModeCfg>;
|
|
58
|
+
adminMode: boolean;
|
|
59
|
+
private listening;
|
|
60
|
+
/**
|
|
61
|
+
* Start listening to keyboard events to toggle AdminMode when detected.
|
|
62
|
+
*/
|
|
63
|
+
startListening(): void;
|
|
64
|
+
stopListening(): void;
|
|
65
|
+
private keydownListener;
|
|
66
|
+
toggleRedDot(): Promise<void>;
|
|
67
|
+
private toggleRedDotVisibility;
|
|
68
|
+
private getRedDotElement;
|
|
69
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdminService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const memo_decorator_1 = require("../decorators/memo.decorator");
|
|
6
|
+
const env_1 = require("../env");
|
|
7
|
+
const stringify_1 = require("../string/stringify");
|
|
8
|
+
const RED_DOT_ID = '__red-dot__';
|
|
9
|
+
const NOOP = () => { };
|
|
10
|
+
/**
|
|
11
|
+
* @experimental
|
|
12
|
+
*
|
|
13
|
+
* Allows to listen for AdminMode keypress combination (Ctrl+Shift+L by default) to toggle AdminMode,
|
|
14
|
+
* indicated by RedDot DOM element.
|
|
15
|
+
*
|
|
16
|
+
* todo: help with Authentication
|
|
17
|
+
*/
|
|
18
|
+
class AdminService {
|
|
19
|
+
constructor(cfg) {
|
|
20
|
+
this.adminMode = false;
|
|
21
|
+
this.listening = false;
|
|
22
|
+
this.cfg = {
|
|
23
|
+
predicate: e => e.ctrlKey && e.key === 'L',
|
|
24
|
+
persistToLocalStorage: true,
|
|
25
|
+
localStorageKey: '__adminMode__',
|
|
26
|
+
onRedDotClick: NOOP,
|
|
27
|
+
onChange: NOOP,
|
|
28
|
+
beforeEnter: () => true,
|
|
29
|
+
beforeExit: () => true,
|
|
30
|
+
...cfg,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start listening to keyboard events to toggle AdminMode when detected.
|
|
35
|
+
*/
|
|
36
|
+
startListening() {
|
|
37
|
+
if (this.listening || (0, env_1.isServerSide)())
|
|
38
|
+
return;
|
|
39
|
+
this.adminMode = !!localStorage.getItem(this.cfg.localStorageKey);
|
|
40
|
+
if (this.adminMode)
|
|
41
|
+
this.toggleRedDotVisibility();
|
|
42
|
+
document.addEventListener('keydown', this.keydownListener.bind(this), { passive: true });
|
|
43
|
+
this.listening = true;
|
|
44
|
+
}
|
|
45
|
+
stopListening() {
|
|
46
|
+
if ((0, env_1.isServerSide)())
|
|
47
|
+
return;
|
|
48
|
+
document.removeEventListener('keydown', this.keydownListener);
|
|
49
|
+
this.listening = false;
|
|
50
|
+
}
|
|
51
|
+
async keydownListener(e) {
|
|
52
|
+
// console.log(e)
|
|
53
|
+
if (!this.cfg.predicate(e))
|
|
54
|
+
return;
|
|
55
|
+
await this.toggleRedDot();
|
|
56
|
+
}
|
|
57
|
+
async toggleRedDot() {
|
|
58
|
+
try {
|
|
59
|
+
const allow = await this.cfg[this.adminMode ? 'beforeExit' : 'beforeEnter']();
|
|
60
|
+
if (!allow)
|
|
61
|
+
return; // no change
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(err);
|
|
65
|
+
// ok to show alert to Admins, it's not user-facing
|
|
66
|
+
alert((0, stringify_1._stringify)(err));
|
|
67
|
+
return; // treat as "not allowed"
|
|
68
|
+
}
|
|
69
|
+
this.adminMode = !this.adminMode;
|
|
70
|
+
this.toggleRedDotVisibility();
|
|
71
|
+
if (this.cfg.persistToLocalStorage) {
|
|
72
|
+
const { localStorageKey } = this.cfg;
|
|
73
|
+
if (this.adminMode) {
|
|
74
|
+
localStorage.setItem(localStorageKey, '1');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
localStorage.removeItem(localStorageKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.cfg.onChange(this.adminMode);
|
|
81
|
+
}
|
|
82
|
+
toggleRedDotVisibility() {
|
|
83
|
+
this.getRedDotElement().style.display = this.adminMode ? 'block' : 'none';
|
|
84
|
+
}
|
|
85
|
+
getRedDotElement() {
|
|
86
|
+
const el = document.createElement('div');
|
|
87
|
+
el.id = RED_DOT_ID;
|
|
88
|
+
el.style.cssText =
|
|
89
|
+
'position:fixed;width:24px;height:24px;margin-top:-12px;background-color:red;opacity:0.5;top:50%;left:0;z-index:9999999;cursor:pointer;border-radius:0 3px 3px 0';
|
|
90
|
+
el.addEventListener('click', () => this.cfg.onRedDotClick());
|
|
91
|
+
document.body.append(el);
|
|
92
|
+
return el;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.AdminService = AdminService;
|
|
96
|
+
tslib_1.__decorate([
|
|
97
|
+
(0, memo_decorator_1._Memo)()
|
|
98
|
+
], AdminService.prototype, "getRedDotElement", null);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
dataLayer: any[];
|
|
4
|
+
gtag: (...args: any[]) => void;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pass enabled = false to only init window.gtag, but not load actual gtag script (e.g in dev mode).
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadGTag(gtagId: string, enabled?: boolean): Promise<void>;
|
|
11
|
+
export declare function loadGTM(gtmId: string, enabled?: boolean): Promise<void>;
|
|
12
|
+
export declare function loadHotjar(hjid: number): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadGTag = loadGTag;
|
|
4
|
+
exports.loadGTM = loadGTM;
|
|
5
|
+
exports.loadHotjar = loadHotjar;
|
|
6
|
+
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
7
|
+
const script_util_1 = require("./script.util");
|
|
8
|
+
/* eslint-disable unicorn/prefer-global-this */
|
|
9
|
+
/**
|
|
10
|
+
* Pass enabled = false to only init window.gtag, but not load actual gtag script (e.g in dev mode).
|
|
11
|
+
*/
|
|
12
|
+
async function loadGTag(gtagId, enabled = true) {
|
|
13
|
+
if ((0, js_lib_1.isServerSide)())
|
|
14
|
+
return;
|
|
15
|
+
window.dataLayer ||= [];
|
|
16
|
+
window.gtag ||= function gtag() {
|
|
17
|
+
// biome-ignore lint/complexity/useArrowFunction: ok
|
|
18
|
+
// biome-ignore lint/style/noArguments: ok
|
|
19
|
+
window.dataLayer.push(arguments);
|
|
20
|
+
};
|
|
21
|
+
window.gtag('js', new Date());
|
|
22
|
+
window.gtag('config', gtagId);
|
|
23
|
+
if (!enabled)
|
|
24
|
+
return;
|
|
25
|
+
await (0, script_util_1.loadScript)(`https://www.googletagmanager.com/gtag/js?id=${gtagId}`);
|
|
26
|
+
}
|
|
27
|
+
async function loadGTM(gtmId, enabled = true) {
|
|
28
|
+
if ((0, js_lib_1.isServerSide)())
|
|
29
|
+
return;
|
|
30
|
+
window.dataLayer ||= [];
|
|
31
|
+
window.dataLayer.push({
|
|
32
|
+
'gtm.start': Date.now(),
|
|
33
|
+
event: 'gtm.js',
|
|
34
|
+
});
|
|
35
|
+
if (!enabled)
|
|
36
|
+
return;
|
|
37
|
+
await (0, script_util_1.loadScript)(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`);
|
|
38
|
+
}
|
|
39
|
+
function loadHotjar(hjid) {
|
|
40
|
+
if ((0, js_lib_1.isServerSide)())
|
|
41
|
+
return;
|
|
42
|
+
;
|
|
43
|
+
((h, o, t, j, a, r) => {
|
|
44
|
+
h.hj =
|
|
45
|
+
h.hj ||
|
|
46
|
+
function hj() {
|
|
47
|
+
// biome-ignore lint/style/noArguments: ok
|
|
48
|
+
;
|
|
49
|
+
(h.hj.q = h.hj.q || []).push(arguments);
|
|
50
|
+
};
|
|
51
|
+
h._hjSettings = { hjid, hjsv: 6 };
|
|
52
|
+
a = o.querySelectorAll('head')[0];
|
|
53
|
+
r = o.createElement('script');
|
|
54
|
+
r.async = 1;
|
|
55
|
+
r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
|
|
56
|
+
a.append(r);
|
|
57
|
+
})(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv=');
|
|
58
|
+
/* eslint-enable */
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Fetcher } from '../../http/fetcher';
|
|
2
|
+
import { StringMap } from '../../types';
|
|
3
|
+
import { TranslationLoader } from './translation.service';
|
|
4
|
+
/**
|
|
5
|
+
* Use `baseUrl` to prefix your language files.
|
|
6
|
+
* Example URL structure:
|
|
7
|
+
* ${baseUrl}/${locale}.json
|
|
8
|
+
*/
|
|
9
|
+
export declare class FetchTranslationLoader implements TranslationLoader {
|
|
10
|
+
fetcher: Fetcher;
|
|
11
|
+
constructor(fetcher: Fetcher);
|
|
12
|
+
load(locale: string): Promise<StringMap>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FetchTranslationLoader = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Use `baseUrl` to prefix your language files.
|
|
6
|
+
* Example URL structure:
|
|
7
|
+
* ${baseUrl}/${locale}.json
|
|
8
|
+
*/
|
|
9
|
+
class FetchTranslationLoader {
|
|
10
|
+
constructor(fetcher) {
|
|
11
|
+
this.fetcher = fetcher;
|
|
12
|
+
}
|
|
13
|
+
async load(locale) {
|
|
14
|
+
return await this.fetcher.get(`${locale}.json`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.FetchTranslationLoader = FetchTranslationLoader;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { StringMap } from '../../types';
|
|
2
|
+
export type MissingTranslationHandler = (key: string, params?: StringMap<any>) => string;
|
|
3
|
+
export declare const defaultMissingTranslationHandler: MissingTranslationHandler;
|
|
4
|
+
export interface TranslationServiceCfg {
|
|
5
|
+
defaultLocale: string;
|
|
6
|
+
supportedLocales: string[];
|
|
7
|
+
/**
|
|
8
|
+
* It is allowed to set it later. Will default to `defaultLocale` in that case.
|
|
9
|
+
*/
|
|
10
|
+
currentLocale?: string;
|
|
11
|
+
translationLoader: TranslationLoader;
|
|
12
|
+
/**
|
|
13
|
+
* Defaults to `defaultMissingTranslationHandler` that returns `[${key}]` and emits console warning.
|
|
14
|
+
*/
|
|
15
|
+
missingTranslationHandler?: MissingTranslationHandler;
|
|
16
|
+
}
|
|
17
|
+
export interface TranslationServiceCfgComplete extends TranslationServiceCfg {
|
|
18
|
+
missingTranslationHandler: MissingTranslationHandler;
|
|
19
|
+
}
|
|
20
|
+
export interface TranslationLoader {
|
|
21
|
+
load: (locale: string) => Promise<StringMap>;
|
|
22
|
+
}
|
|
23
|
+
export declare class TranslationService {
|
|
24
|
+
constructor(cfg: TranslationServiceCfg, preloadedLocales?: StringMap<StringMap>);
|
|
25
|
+
cfg: TranslationServiceCfgComplete;
|
|
26
|
+
/**
|
|
27
|
+
* Cache of loaded locales
|
|
28
|
+
*/
|
|
29
|
+
locales: StringMap<StringMap>;
|
|
30
|
+
currentLocale: string;
|
|
31
|
+
/**
|
|
32
|
+
* Manually set locale data, bypassing the TranslationLoader.
|
|
33
|
+
*/
|
|
34
|
+
setLocale(localeName: string, locale: StringMap): void;
|
|
35
|
+
getLocale(locale: string): StringMap | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Loads locale(s) (if not already cached) via configured TranslationLoader.
|
|
38
|
+
* Resolves promise when done (ready to be used).
|
|
39
|
+
*/
|
|
40
|
+
loadLocale(locale: string | string[]): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Will invoke `missingTranslationHandler` on missing tranlation.
|
|
43
|
+
*
|
|
44
|
+
* Does NOT do any locale loading. The locale needs to be loaded beforehand:
|
|
45
|
+
* either pre-loaded and passed to the constructor,
|
|
46
|
+
* or `await loadLocale(locale)`.
|
|
47
|
+
*/
|
|
48
|
+
translate(key: string, params?: StringMap): string;
|
|
49
|
+
/**
|
|
50
|
+
* Does NOT invoke `missingTranslationHandler`, returns `undefined` instead.
|
|
51
|
+
*/
|
|
52
|
+
translateIfExists(key: string, _params?: StringMap): string | undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TranslationService = exports.defaultMissingTranslationHandler = void 0;
|
|
4
|
+
const pMap_1 = require("../../promise/pMap");
|
|
5
|
+
const defaultMissingTranslationHandler = key => {
|
|
6
|
+
console.warn(`[tr] missing: ${key}`);
|
|
7
|
+
return `[${key}]`;
|
|
8
|
+
};
|
|
9
|
+
exports.defaultMissingTranslationHandler = defaultMissingTranslationHandler;
|
|
10
|
+
class TranslationService {
|
|
11
|
+
constructor(cfg, preloadedLocales = {}) {
|
|
12
|
+
this.cfg = {
|
|
13
|
+
...cfg,
|
|
14
|
+
missingTranslationHandler: exports.defaultMissingTranslationHandler,
|
|
15
|
+
};
|
|
16
|
+
this.locales = {
|
|
17
|
+
...preloadedLocales,
|
|
18
|
+
};
|
|
19
|
+
this.currentLocale = cfg.currentLocale || cfg.defaultLocale;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Manually set locale data, bypassing the TranslationLoader.
|
|
23
|
+
*/
|
|
24
|
+
setLocale(localeName, locale) {
|
|
25
|
+
this.locales[localeName] = locale;
|
|
26
|
+
}
|
|
27
|
+
getLocale(locale) {
|
|
28
|
+
return this.locales[locale];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Loads locale(s) (if not already cached) via configured TranslationLoader.
|
|
32
|
+
* Resolves promise when done (ready to be used).
|
|
33
|
+
*/
|
|
34
|
+
async loadLocale(locale) {
|
|
35
|
+
const locales = Array.isArray(locale) ? locale : [locale];
|
|
36
|
+
await (0, pMap_1.pMap)(locales, async (locale) => {
|
|
37
|
+
if (this.locales[locale])
|
|
38
|
+
return; // already loaded
|
|
39
|
+
this.locales[locale] = await this.cfg.translationLoader.load(locale);
|
|
40
|
+
// console.log(`[tr] locale loaded: ${locale}`)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Will invoke `missingTranslationHandler` on missing tranlation.
|
|
45
|
+
*
|
|
46
|
+
* Does NOT do any locale loading. The locale needs to be loaded beforehand:
|
|
47
|
+
* either pre-loaded and passed to the constructor,
|
|
48
|
+
* or `await loadLocale(locale)`.
|
|
49
|
+
*/
|
|
50
|
+
translate(key, params) {
|
|
51
|
+
return this.translateIfExists(key, params) || this.cfg.missingTranslationHandler(key, params);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Does NOT invoke `missingTranslationHandler`, returns `undefined` instead.
|
|
55
|
+
*/
|
|
56
|
+
translateIfExists(key, _params) {
|
|
57
|
+
// todo: support params
|
|
58
|
+
return this.locales[this.currentLocale]?.[key] || this.locales[this.cfg.defaultLocale]?.[key];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.TranslationService = TranslationService;
|