@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.
Files changed (60) hide show
  1. package/cfg/frontend/tsconfig.json +67 -0
  2. package/dist/bot.d.ts +60 -0
  3. package/dist/bot.js +129 -0
  4. package/dist/browser/adminService.d.ts +69 -0
  5. package/dist/browser/adminService.js +98 -0
  6. package/dist/browser/analytics.util.d.ts +12 -0
  7. package/dist/browser/analytics.util.js +59 -0
  8. package/dist/browser/i18n/fetchTranslationLoader.d.ts +13 -0
  9. package/dist/browser/i18n/fetchTranslationLoader.js +17 -0
  10. package/dist/browser/i18n/translation.service.d.ts +53 -0
  11. package/dist/browser/i18n/translation.service.js +61 -0
  12. package/dist/browser/imageFitter.d.ts +60 -0
  13. package/dist/browser/imageFitter.js +69 -0
  14. package/dist/browser/script.util.d.ts +14 -0
  15. package/dist/browser/script.util.js +50 -0
  16. package/dist/browser/topbar.d.ts +23 -0
  17. package/dist/browser/topbar.js +137 -0
  18. package/dist/decorators/memo.util.d.ts +2 -1
  19. package/dist/decorators/memo.util.js +8 -6
  20. package/dist/decorators/swarmSafe.decorator.d.ts +9 -0
  21. package/dist/decorators/swarmSafe.decorator.js +42 -0
  22. package/dist/error/assert.d.ts +2 -1
  23. package/dist/error/assert.js +15 -13
  24. package/dist/error/error.util.js +9 -6
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.js +8 -0
  27. package/dist/zod/zod.util.d.ts +1 -1
  28. package/dist-esm/bot.js +125 -0
  29. package/dist-esm/browser/adminService.js +94 -0
  30. package/dist-esm/browser/analytics.util.js +54 -0
  31. package/dist-esm/browser/i18n/fetchTranslationLoader.js +13 -0
  32. package/dist-esm/browser/i18n/translation.service.js +56 -0
  33. package/dist-esm/browser/imageFitter.js +65 -0
  34. package/dist-esm/browser/script.util.js +46 -0
  35. package/dist-esm/browser/topbar.js +134 -0
  36. package/dist-esm/decorators/memo.util.js +3 -1
  37. package/dist-esm/decorators/swarmSafe.decorator.js +38 -0
  38. package/dist-esm/error/assert.js +3 -1
  39. package/dist-esm/error/error.util.js +4 -1
  40. package/dist-esm/index.js +8 -0
  41. package/package.json +2 -1
  42. package/src/bot.ts +155 -0
  43. package/src/browser/adminService.ts +157 -0
  44. package/src/browser/analytics.util.ts +68 -0
  45. package/src/browser/i18n/fetchTranslationLoader.ts +16 -0
  46. package/src/browser/i18n/translation.service.ts +102 -0
  47. package/src/browser/imageFitter.ts +128 -0
  48. package/src/browser/script.util.ts +52 -0
  49. package/src/browser/topbar.ts +147 -0
  50. package/src/datetime/localDate.ts +16 -0
  51. package/src/datetime/localTime.ts +39 -0
  52. package/src/decorators/debounce.ts +1 -0
  53. package/src/decorators/memo.util.ts +4 -1
  54. package/src/decorators/swarmSafe.decorator.ts +47 -0
  55. package/src/error/assert.ts +5 -11
  56. package/src/error/error.util.ts +4 -1
  57. package/src/index.ts +8 -0
  58. package/src/json-schema/jsonSchemaBuilder.ts +20 -0
  59. package/src/semver.ts +2 -0
  60. 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;