@lionrapid/core 0.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 +106 -0
- package/dist/base/event-emitter.d.ts +91 -0
- package/dist/base/event-emitter.d.ts.map +1 -0
- package/dist/base/event-emitter.js +330 -0
- package/dist/base/event-emitter.js.map +1 -0
- package/dist/base/index.d.ts +5 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +5 -0
- package/dist/base/index.js.map +1 -0
- package/dist/browser/auto-init.d.ts +57 -0
- package/dist/browser/auto-init.d.ts.map +1 -0
- package/dist/browser/auto-init.js +304 -0
- package/dist/browser/auto-init.js.map +1 -0
- package/dist/browser/auto-translator.d.ts +98 -0
- package/dist/browser/auto-translator.d.ts.map +1 -0
- package/dist/browser/auto-translator.js +218 -0
- package/dist/browser/auto-translator.js.map +1 -0
- package/dist/browser/block-extractor.d.ts +26 -0
- package/dist/browser/block-extractor.d.ts.map +1 -0
- package/dist/browser/block-extractor.js +143 -0
- package/dist/browser/block-extractor.js.map +1 -0
- package/dist/browser/dom-observer.d.ts +42 -0
- package/dist/browser/dom-observer.d.ts.map +1 -0
- package/dist/browser/dom-observer.js +109 -0
- package/dist/browser/dom-observer.js.map +1 -0
- package/dist/browser/editor/editor-api.d.ts +67 -0
- package/dist/browser/editor/editor-api.d.ts.map +1 -0
- package/dist/browser/editor/editor-api.js +233 -0
- package/dist/browser/editor/editor-api.js.map +1 -0
- package/dist/browser/editor/editor-init.d.ts +11 -0
- package/dist/browser/editor/editor-init.d.ts.map +1 -0
- package/dist/browser/editor/editor-init.js +181 -0
- package/dist/browser/editor/editor-init.js.map +1 -0
- package/dist/browser/editor/editor-styles.d.ts +6 -0
- package/dist/browser/editor/editor-styles.d.ts.map +1 -0
- package/dist/browser/editor/editor-styles.js +419 -0
- package/dist/browser/editor/editor-styles.js.map +1 -0
- package/dist/browser/editor/inspector.d.ts +22 -0
- package/dist/browser/editor/inspector.d.ts.map +1 -0
- package/dist/browser/editor/inspector.js +125 -0
- package/dist/browser/editor/inspector.js.map +1 -0
- package/dist/browser/editor/key-resolver.d.ts +19 -0
- package/dist/browser/editor/key-resolver.d.ts.map +1 -0
- package/dist/browser/editor/key-resolver.js +49 -0
- package/dist/browser/editor/key-resolver.js.map +1 -0
- package/dist/browser/editor/live-preview.d.ts +10 -0
- package/dist/browser/editor/live-preview.d.ts.map +1 -0
- package/dist/browser/editor/live-preview.js +57 -0
- package/dist/browser/editor/live-preview.js.map +1 -0
- package/dist/browser/editor/sidebar.d.ts +50 -0
- package/dist/browser/editor/sidebar.d.ts.map +1 -0
- package/dist/browser/editor/sidebar.js +353 -0
- package/dist/browser/editor/sidebar.js.map +1 -0
- package/dist/browser/editor/toolbar.d.ts +29 -0
- package/dist/browser/editor/toolbar.d.ts.map +1 -0
- package/dist/browser/editor/toolbar.js +131 -0
- package/dist/browser/editor/toolbar.js.map +1 -0
- package/dist/browser/exclude-filter.d.ts +21 -0
- package/dist/browser/exclude-filter.d.ts.map +1 -0
- package/dist/browser/exclude-filter.js +51 -0
- package/dist/browser/exclude-filter.js.map +1 -0
- package/dist/browser/index.d.ts +32 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +28 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/language-switcher.d.ts +70 -0
- package/dist/browser/language-switcher.d.ts.map +1 -0
- package/dist/browser/language-switcher.js +339 -0
- package/dist/browser/language-switcher.js.map +1 -0
- package/dist/browser/lionrapid.auto.dev.js +5757 -0
- package/dist/browser/lionrapid.auto.dev.js.map +7 -0
- package/dist/browser/lionrapid.auto.js +5757 -0
- package/dist/browser/lionrapid.auto.js.map +7 -0
- package/dist/browser/lionrapid.auto.min.js +80 -0
- package/dist/browser/lionrapid.auto.min.js.map +7 -0
- package/dist/browser/lionrapid.editor.js +1869 -0
- package/dist/browser/lionrapid.editor.js.map +7 -0
- package/dist/browser/lionrapid.editor.min.js +415 -0
- package/dist/browser/lionrapid.editor.min.js.map +7 -0
- package/dist/browser/lionrapid.esm.js +4786 -0
- package/dist/browser/lionrapid.esm.js.map +7 -0
- package/dist/browser/lionrapid.esm.min.js +3 -0
- package/dist/browser/lionrapid.esm.min.js.map +7 -0
- package/dist/browser/lionrapid.iife.dev.js +4809 -0
- package/dist/browser/lionrapid.iife.dev.js.map +7 -0
- package/dist/browser/lionrapid.iife.js +4809 -0
- package/dist/browser/lionrapid.iife.js.map +7 -0
- package/dist/browser/lionrapid.iife.min.js +3 -0
- package/dist/browser/lionrapid.iife.min.js.map +7 -0
- package/dist/browser/lionrapid.umd.js +4822 -0
- package/dist/browser/lionrapid.umd.js.map +7 -0
- package/dist/browser/lionrapid.umd.min.js +18 -0
- package/dist/browser/lionrapid.umd.min.js.map +7 -0
- package/dist/browser/markup-parser.d.ts +13 -0
- package/dist/browser/markup-parser.d.ts.map +1 -0
- package/dist/browser/markup-parser.js +56 -0
- package/dist/browser/markup-parser.js.map +1 -0
- package/dist/browser/md5.d.ts +16 -0
- package/dist/browser/md5.d.ts.map +1 -0
- package/dist/browser/md5.js +145 -0
- package/dist/browser/md5.js.map +1 -0
- package/dist/browser/storage/cache-storage-repository.d.ts +44 -0
- package/dist/browser/storage/cache-storage-repository.d.ts.map +1 -0
- package/dist/browser/storage/cache-storage-repository.js +186 -0
- package/dist/browser/storage/cache-storage-repository.js.map +1 -0
- package/dist/browser/storage/localstorage-repository.d.ts +51 -0
- package/dist/browser/storage/localstorage-repository.d.ts.map +1 -0
- package/dist/browser/storage/localstorage-repository.js +270 -0
- package/dist/browser/storage/localstorage-repository.js.map +1 -0
- package/dist/builder.d.ts +24 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +169 -0
- package/dist/builder.js.map +1 -0
- package/dist/cache/CacheManager.d.ts +132 -0
- package/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +277 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/lionrapid.config.d.ts +12 -0
- package/dist/config/lionrapid.config.d.ts.map +1 -0
- package/dist/config/lionrapid.config.js +2 -0
- package/dist/config/lionrapid.config.js.map +1 -0
- package/dist/config/logger.config.d.ts +19 -0
- package/dist/config/logger.config.d.ts.map +1 -0
- package/dist/config/logger.config.js +9 -0
- package/dist/config/logger.config.js.map +1 -0
- package/dist/config/repository.config.d.ts +87 -0
- package/dist/config/repository.config.d.ts.map +1 -0
- package/dist/config/repository.config.js +5 -0
- package/dist/config/repository.config.js.map +1 -0
- package/dist/content/index.d.ts +20 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +13 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/parser/IParser.d.ts +33 -0
- package/dist/content/parser/IParser.d.ts.map +1 -0
- package/dist/content/parser/IParser.js +6 -0
- package/dist/content/parser/IParser.js.map +1 -0
- package/dist/content/parser/MarkupParser.d.ts +67 -0
- package/dist/content/parser/MarkupParser.d.ts.map +1 -0
- package/dist/content/parser/MarkupParser.js +231 -0
- package/dist/content/parser/MarkupParser.js.map +1 -0
- package/dist/content/serializer/HTMLSerializer.d.ts +61 -0
- package/dist/content/serializer/HTMLSerializer.d.ts.map +1 -0
- package/dist/content/serializer/HTMLSerializer.js +100 -0
- package/dist/content/serializer/HTMLSerializer.js.map +1 -0
- package/dist/content/serializer/ISerializer.d.ts +33 -0
- package/dist/content/serializer/ISerializer.d.ts.map +1 -0
- package/dist/content/serializer/ISerializer.js +6 -0
- package/dist/content/serializer/ISerializer.js.map +1 -0
- package/dist/content/style-mapper/DefaultStyleMapper.d.ts +59 -0
- package/dist/content/style-mapper/DefaultStyleMapper.d.ts.map +1 -0
- package/dist/content/style-mapper/DefaultStyleMapper.js +128 -0
- package/dist/content/style-mapper/DefaultStyleMapper.js.map +1 -0
- package/dist/content/style-mapper/IStyleMapper.d.ts +50 -0
- package/dist/content/style-mapper/IStyleMapper.d.ts.map +1 -0
- package/dist/content/style-mapper/IStyleMapper.js +6 -0
- package/dist/content/style-mapper/IStyleMapper.js.map +1 -0
- package/dist/content/types.d.ts +39 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +6 -0
- package/dist/content/types.js.map +1 -0
- package/dist/content/validator/ContentValidator.d.ts +57 -0
- package/dist/content/validator/ContentValidator.d.ts.map +1 -0
- package/dist/content/validator/ContentValidator.js +215 -0
- package/dist/content/validator/ContentValidator.js.map +1 -0
- package/dist/content/validator/IValidator.d.ts +82 -0
- package/dist/content/validator/IValidator.d.ts.map +1 -0
- package/dist/content/validator/IValidator.js +6 -0
- package/dist/content/validator/IValidator.js.map +1 -0
- package/dist/errors/translation-errors.d.ts +55 -0
- package/dist/errors/translation-errors.d.ts.map +1 -0
- package/dist/errors/translation-errors.js +85 -0
- package/dist/errors/translation-errors.js.map +1 -0
- package/dist/handlers/async-storage-handler.d.ts +77 -0
- package/dist/handlers/async-storage-handler.d.ts.map +1 -0
- package/dist/handlers/async-storage-handler.js +239 -0
- package/dist/handlers/async-storage-handler.js.map +1 -0
- package/dist/handlers/handler-chain.d.ts +79 -0
- package/dist/handlers/handler-chain.d.ts.map +1 -0
- package/dist/handlers/handler-chain.js +366 -0
- package/dist/handlers/handler-chain.js.map +1 -0
- package/dist/handlers/index.d.ts +7 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/network-handler.d.ts +90 -0
- package/dist/handlers/network-handler.d.ts.map +1 -0
- package/dist/handlers/network-handler.js +283 -0
- package/dist/handlers/network-handler.js.map +1 -0
- package/dist/handlers/sync-storage-handler.d.ts +88 -0
- package/dist/handlers/sync-storage-handler.d.ts.map +1 -0
- package/dist/handlers/sync-storage-handler.js +270 -0
- package/dist/handlers/sync-storage-handler.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/emitter.interface.d.ts +33 -0
- package/dist/interfaces/emitter.interface.d.ts.map +1 -0
- package/dist/interfaces/emitter.interface.js +2 -0
- package/dist/interfaces/emitter.interface.js.map +1 -0
- package/dist/interfaces/handler.interface.d.ts +50 -0
- package/dist/interfaces/handler.interface.d.ts.map +1 -0
- package/dist/interfaces/handler.interface.js +5 -0
- package/dist/interfaces/handler.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/plugin.interface.d.ts +49 -0
- package/dist/interfaces/plugin.interface.d.ts.map +1 -0
- package/dist/interfaces/plugin.interface.js +6 -0
- package/dist/interfaces/plugin.interface.js.map +1 -0
- package/dist/interfaces/repository.interface.d.ts +56 -0
- package/dist/interfaces/repository.interface.d.ts.map +1 -0
- package/dist/interfaces/repository.interface.js +2 -0
- package/dist/interfaces/repository.interface.js.map +1 -0
- package/dist/lionRapid.d.ts +225 -0
- package/dist/lionRapid.d.ts.map +1 -0
- package/dist/lionRapid.js +596 -0
- package/dist/lionRapid.js.map +1 -0
- package/dist/repositories/index.d.ts +3 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +3 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/repositories/memory-repository.d.ts +30 -0
- package/dist/repositories/memory-repository.d.ts.map +1 -0
- package/dist/repositories/memory-repository.js +100 -0
- package/dist/repositories/memory-repository.js.map +1 -0
- package/dist/repositories/network-repository.d.ts +138 -0
- package/dist/repositories/network-repository.d.ts.map +1 -0
- package/dist/repositories/network-repository.js +516 -0
- package/dist/repositories/network-repository.js.map +1 -0
- package/dist/sync/missing-translation-queue.d.ts +64 -0
- package/dist/sync/missing-translation-queue.d.ts.map +1 -0
- package/dist/sync/missing-translation-queue.js +184 -0
- package/dist/sync/missing-translation-queue.js.map +1 -0
- package/dist/types/cache.types.d.ts +62 -0
- package/dist/types/cache.types.d.ts.map +1 -0
- package/dist/types/cache.types.js +6 -0
- package/dist/types/cache.types.js.map +1 -0
- package/dist/types/event-payloads.types.d.ts +46 -0
- package/dist/types/event-payloads.types.d.ts.map +1 -0
- package/dist/types/event-payloads.types.js +6 -0
- package/dist/types/event-payloads.types.js.map +1 -0
- package/dist/types/generated.types.d.ts +31 -0
- package/dist/types/generated.types.d.ts.map +1 -0
- package/dist/types/generated.types.js +6 -0
- package/dist/types/generated.types.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lionRapid.types.d.ts +169 -0
- package/dist/types/lionRapid.types.d.ts.map +1 -0
- package/dist/types/lionRapid.types.js +8 -0
- package/dist/types/lionRapid.types.js.map +1 -0
- package/dist/types/parameters.types.d.ts +36 -0
- package/dist/types/parameters.types.d.ts.map +1 -0
- package/dist/types/parameters.types.js +6 -0
- package/dist/types/parameters.types.js.map +1 -0
- package/dist/types/translation-data.types.d.ts +54 -0
- package/dist/types/translation-data.types.d.ts.map +1 -0
- package/dist/types/translation-data.types.js +6 -0
- package/dist/types/translation-data.types.js.map +1 -0
- package/dist/types/translation.types.d.ts +94 -0
- package/dist/types/translation.types.d.ts.map +1 -0
- package/dist/types/translation.types.js +5 -0
- package/dist/types/translation.types.js.map +1 -0
- package/dist/utils/context-utils.d.ts +23 -0
- package/dist/utils/context-utils.d.ts.map +1 -0
- package/dist/utils/context-utils.js +36 -0
- package/dist/utils/context-utils.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +19 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +229 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +345 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/namespace-utils.d.ts +30 -0
- package/dist/utils/namespace-utils.d.ts.map +1 -0
- package/dist/utils/namespace-utils.js +56 -0
- package/dist/utils/namespace-utils.js.map +1 -0
- package/dist/utils/translation-flow.d.ts +18 -0
- package/dist/utils/translation-flow.d.ts.map +1 -0
- package/dist/utils/translation-flow.js +74 -0
- package/dist/utils/translation-flow.js.map +1 -0
- package/dist/utils/translation-value-utils.d.ts +57 -0
- package/dist/utils/translation-value-utils.d.ts.map +1 -0
- package/dist/utils/translation-value-utils.js +194 -0
- package/dist/utils/translation-value-utils.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,4822 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof define === 'function' && define.amd) {
|
|
3
|
+
// AMD
|
|
4
|
+
define(['exports'], factory);
|
|
5
|
+
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
|
|
6
|
+
// CommonJS
|
|
7
|
+
factory(exports);
|
|
8
|
+
} else {
|
|
9
|
+
// Browser globals
|
|
10
|
+
factory((root.LionRapid = {}));
|
|
11
|
+
}
|
|
12
|
+
}(typeof self !== 'undefined' ? self : this, function (exports) {
|
|
13
|
+
"use strict";
|
|
14
|
+
var __defProp = Object.defineProperty;
|
|
15
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
17
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
18
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
+
for (let key of __getOwnPropNames(from))
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
+
}
|
|
29
|
+
return to;
|
|
30
|
+
};
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
33
|
+
|
|
34
|
+
// src/index.ts
|
|
35
|
+
var index_exports = {};
|
|
36
|
+
__export(index_exports, {
|
|
37
|
+
CacheManager: () => CacheManager,
|
|
38
|
+
CacheStorageRepository: () => CacheStorageRepository,
|
|
39
|
+
Colors: () => Colors,
|
|
40
|
+
ContentValidator: () => ContentValidator,
|
|
41
|
+
DefaultStyleMapper: () => DefaultStyleMapper,
|
|
42
|
+
EventEmitter: () => EventEmitter,
|
|
43
|
+
HTMLSerializer: () => HTMLSerializer,
|
|
44
|
+
HandlerChain: () => HandlerChain,
|
|
45
|
+
LionRapidBuilder: () => LionRapidBuilder,
|
|
46
|
+
LionRapidCore: () => LionRapidCore,
|
|
47
|
+
LocalStorageRepository: () => LocalStorageRepository,
|
|
48
|
+
LogLevel: () => LogLevel,
|
|
49
|
+
Logger: () => Logger,
|
|
50
|
+
MarkupParser: () => MarkupParser,
|
|
51
|
+
MemoryRepository: () => MemoryRepository,
|
|
52
|
+
MissingTranslationQueue: () => MissingTranslationQueue,
|
|
53
|
+
NetworkError: () => NetworkError,
|
|
54
|
+
NetworkRepository: () => NetworkRepository,
|
|
55
|
+
ServerError: () => ServerError,
|
|
56
|
+
TranslationError: () => TranslationError,
|
|
57
|
+
TranslationLockedError: () => TranslationLockedError,
|
|
58
|
+
TranslationNotFoundError: () => TranslationNotFoundError,
|
|
59
|
+
VERSION: () => VERSION,
|
|
60
|
+
createPackageLogger: () => createPackageLogger,
|
|
61
|
+
isClientError: () => isClientError,
|
|
62
|
+
isRetryableError: () => isRetryableError,
|
|
63
|
+
isTranslationError: () => isTranslationError,
|
|
64
|
+
logger: () => logger,
|
|
65
|
+
measure: () => measure,
|
|
66
|
+
parseKeyPath: () => parseKeyPath,
|
|
67
|
+
prepareTranslationRequest: () => prepareTranslationRequest,
|
|
68
|
+
resolveNestedKey: () => resolveNestedKey
|
|
69
|
+
});
|
|
70
|
+
module.exports = __toCommonJS(index_exports);
|
|
71
|
+
|
|
72
|
+
// src/config/logger.config.ts
|
|
73
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
74
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
75
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
76
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
77
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
78
|
+
LogLevel2[LogLevel2["NONE"] = 4] = "NONE";
|
|
79
|
+
return LogLevel2;
|
|
80
|
+
})(LogLevel || {});
|
|
81
|
+
|
|
82
|
+
// src/utils/logger.ts
|
|
83
|
+
var IS_PRODUCTION = true;
|
|
84
|
+
var IS_DEVELOPMENT = false;
|
|
85
|
+
var IS_CI = false;
|
|
86
|
+
var LIONRAPID_DEV = false;
|
|
87
|
+
var PERFORMANCE_TRACKING = false;
|
|
88
|
+
var LOG_LEVEL_ENV = IS_PRODUCTION ? "ERROR" : "DEBUG";
|
|
89
|
+
var Colors = {
|
|
90
|
+
// Reset
|
|
91
|
+
RESET: "\x1B[0m",
|
|
92
|
+
// Foreground colors
|
|
93
|
+
BLACK: "\x1B[30m",
|
|
94
|
+
RED: "\x1B[31m",
|
|
95
|
+
GREEN: "\x1B[32m",
|
|
96
|
+
YELLOW: "\x1B[33m",
|
|
97
|
+
BLUE: "\x1B[34m",
|
|
98
|
+
MAGENTA: "\x1B[35m",
|
|
99
|
+
CYAN: "\x1B[36m",
|
|
100
|
+
WHITE: "\x1B[37m",
|
|
101
|
+
ORANGE: "\x1B[38;5;208m",
|
|
102
|
+
// Bright foreground colors
|
|
103
|
+
BRIGHT_BLACK: "\x1B[90m",
|
|
104
|
+
BRIGHT_RED: "\x1B[91m",
|
|
105
|
+
BRIGHT_GREEN: "\x1B[92m",
|
|
106
|
+
BRIGHT_YELLOW: "\x1B[93m",
|
|
107
|
+
BRIGHT_BLUE: "\x1B[94m",
|
|
108
|
+
BRIGHT_MAGENTA: "\x1B[95m",
|
|
109
|
+
BRIGHT_CYAN: "\x1B[96m",
|
|
110
|
+
BRIGHT_WHITE: "\x1B[97m",
|
|
111
|
+
// Styles
|
|
112
|
+
BOLD: "\x1B[1m",
|
|
113
|
+
DIM: "\x1B[2m",
|
|
114
|
+
UNDERLINE: "\x1B[4m"
|
|
115
|
+
};
|
|
116
|
+
var DevelopmentLogger = class _DevelopmentLogger {
|
|
117
|
+
constructor(config = {}) {
|
|
118
|
+
__publicField(this, "config");
|
|
119
|
+
this.config = {
|
|
120
|
+
level: this.getLogLevelFromEnv(),
|
|
121
|
+
enabled: this.shouldEnableLogging(),
|
|
122
|
+
prefix: `${config.packageEmoji || "\u{1F4E6}"} ${config.packageName || "LionRapid"}`,
|
|
123
|
+
showLineNumbers: LIONRAPID_DEV && !IS_CI,
|
|
124
|
+
useColors: this.shouldUseColors(),
|
|
125
|
+
...config
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
configure(config) {
|
|
129
|
+
this.config = { ...this.config, ...config };
|
|
130
|
+
}
|
|
131
|
+
debug(message, data) {
|
|
132
|
+
if (!this.config.enabled || this.config.level > 0 /* DEBUG */) return;
|
|
133
|
+
this.log(0 /* DEBUG */, message, data);
|
|
134
|
+
}
|
|
135
|
+
info(message, data) {
|
|
136
|
+
if (this.config.level > 1 /* INFO */) return;
|
|
137
|
+
this.log(1 /* INFO */, message, data);
|
|
138
|
+
}
|
|
139
|
+
warn(message, data) {
|
|
140
|
+
this.log(2 /* WARN */, message, data);
|
|
141
|
+
}
|
|
142
|
+
error(message, data) {
|
|
143
|
+
this.log(3 /* ERROR */, message, data);
|
|
144
|
+
}
|
|
145
|
+
time(label) {
|
|
146
|
+
if (!PERFORMANCE_TRACKING || IS_PRODUCTION) return () => {
|
|
147
|
+
};
|
|
148
|
+
const start = IS_DEVELOPMENT ? performance.now() : 0;
|
|
149
|
+
return () => {
|
|
150
|
+
if (IS_DEVELOPMENT) {
|
|
151
|
+
const duration = performance.now() - start;
|
|
152
|
+
this.debug(`\u23F1\uFE0F ${label}: ${duration.toFixed(2)}ms`, { duration });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
log(level, message, data) {
|
|
157
|
+
if (!this.config.enabled || level < this.config.level) return;
|
|
158
|
+
const levelName = LogLevel[level];
|
|
159
|
+
const levelEmoji = this.getLevelEmoji(level);
|
|
160
|
+
const callSite = this.config.showLineNumbers ? this.getCallSite() : null;
|
|
161
|
+
const logMessage = this.buildColoredMessage(
|
|
162
|
+
level,
|
|
163
|
+
levelEmoji,
|
|
164
|
+
levelName,
|
|
165
|
+
message,
|
|
166
|
+
callSite,
|
|
167
|
+
data
|
|
168
|
+
);
|
|
169
|
+
switch (level) {
|
|
170
|
+
case 3 /* ERROR */:
|
|
171
|
+
console.error(logMessage);
|
|
172
|
+
break;
|
|
173
|
+
case 2 /* WARN */:
|
|
174
|
+
console.warn(logMessage);
|
|
175
|
+
break;
|
|
176
|
+
case 1 /* INFO */:
|
|
177
|
+
console.info(logMessage);
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
console.log(logMessage);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
buildColoredMessage(level, emoji, levelName, message, callSite, data) {
|
|
184
|
+
if (!this.config.useColors) {
|
|
185
|
+
const locationInfo2 = callSite ? ` (${callSite.fileName}:${callSite.lineNumber})` : "";
|
|
186
|
+
let logMessage = `${this.config.prefix} ${emoji} ${levelName} ${message}${locationInfo2}`;
|
|
187
|
+
if (data !== void 0) {
|
|
188
|
+
logMessage += ` ${JSON.stringify(data)}`;
|
|
189
|
+
}
|
|
190
|
+
return logMessage;
|
|
191
|
+
}
|
|
192
|
+
const levelColor = this.getLevelColor(level);
|
|
193
|
+
const reset = Colors.RESET;
|
|
194
|
+
const coloredPrefix = `${Colors.BOLD}${Colors.ORANGE}${this.config.prefix}${reset}`;
|
|
195
|
+
const coloredEmoji = emoji;
|
|
196
|
+
const coloredLevel = `${levelColor}${Colors.BOLD}${levelName}${reset}`;
|
|
197
|
+
const coloredMessage = `${levelColor}${message}${reset}`;
|
|
198
|
+
const locationInfo = callSite ? `${Colors.DIM}${Colors.BRIGHT_BLACK} (${callSite.fileName}:${callSite.lineNumber})${reset}` : "";
|
|
199
|
+
const dataInfo = data !== void 0 ? `${Colors.DIM}${Colors.CYAN} ${JSON.stringify(data)}${reset}` : "";
|
|
200
|
+
return `${coloredPrefix} ${coloredEmoji} ${coloredLevel} ${coloredMessage}${locationInfo}${dataInfo}`;
|
|
201
|
+
}
|
|
202
|
+
getLevelColor(level) {
|
|
203
|
+
switch (level) {
|
|
204
|
+
case 0 /* DEBUG */:
|
|
205
|
+
return Colors.BRIGHT_GREEN;
|
|
206
|
+
case 1 /* INFO */:
|
|
207
|
+
return Colors.BRIGHT_CYAN;
|
|
208
|
+
case 2 /* WARN */:
|
|
209
|
+
return Colors.BRIGHT_YELLOW;
|
|
210
|
+
case 3 /* ERROR */:
|
|
211
|
+
return Colors.BRIGHT_RED;
|
|
212
|
+
default:
|
|
213
|
+
return Colors.BRIGHT_BLACK;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
getLevelEmoji(level) {
|
|
217
|
+
switch (level) {
|
|
218
|
+
case 0 /* DEBUG */:
|
|
219
|
+
return "\u{1F50D}";
|
|
220
|
+
case 1 /* INFO */:
|
|
221
|
+
return "\u2139\uFE0F";
|
|
222
|
+
case 2 /* WARN */:
|
|
223
|
+
return "\u26A0\uFE0F";
|
|
224
|
+
case 3 /* ERROR */:
|
|
225
|
+
return "\u274C";
|
|
226
|
+
default:
|
|
227
|
+
return "\u{1F4DD}";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
getCallSite() {
|
|
231
|
+
try {
|
|
232
|
+
const stack = new Error().stack;
|
|
233
|
+
if (!stack) return null;
|
|
234
|
+
const lines = stack.split("\n");
|
|
235
|
+
for (let i = 3; i < lines.length; i++) {
|
|
236
|
+
const line = lines[i];
|
|
237
|
+
if (line && !line.includes("logger.ts") && !line.includes("Logger")) {
|
|
238
|
+
return this.parseStackLine(line);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
parseStackLine(line) {
|
|
246
|
+
let match = line.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/);
|
|
247
|
+
if (match) {
|
|
248
|
+
return {
|
|
249
|
+
functionName: match[1],
|
|
250
|
+
fileName: this.extractFileName(match[2]),
|
|
251
|
+
lineNumber: parseInt(match[3], 10)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
match = line.match(/at\s+(.+):(\d+):(\d+)/);
|
|
255
|
+
if (match) {
|
|
256
|
+
return {
|
|
257
|
+
fileName: this.extractFileName(match[1]),
|
|
258
|
+
lineNumber: parseInt(match[2], 10)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
extractFileName(filePath) {
|
|
264
|
+
const parts = filePath.split("/");
|
|
265
|
+
return parts[parts.length - 1] || filePath;
|
|
266
|
+
}
|
|
267
|
+
colorize(text, color) {
|
|
268
|
+
if (!this.config.useColors) return text;
|
|
269
|
+
return `${Colors[color]}${text}${Colors.RESET}`;
|
|
270
|
+
}
|
|
271
|
+
getColors() {
|
|
272
|
+
return Colors;
|
|
273
|
+
}
|
|
274
|
+
createPackageLogger(packageName, packageEmoji) {
|
|
275
|
+
return new _DevelopmentLogger({
|
|
276
|
+
...this.config,
|
|
277
|
+
packageName,
|
|
278
|
+
packageEmoji: packageEmoji || "\u{1F4E6}"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
getLogLevelFromEnv() {
|
|
282
|
+
switch (LOG_LEVEL_ENV.toUpperCase()) {
|
|
283
|
+
case "DEBUG":
|
|
284
|
+
return 0 /* DEBUG */;
|
|
285
|
+
case "INFO":
|
|
286
|
+
return 1 /* INFO */;
|
|
287
|
+
case "WARN":
|
|
288
|
+
return 2 /* WARN */;
|
|
289
|
+
case "ERROR":
|
|
290
|
+
return 3 /* ERROR */;
|
|
291
|
+
case "NONE":
|
|
292
|
+
return 4 /* NONE */;
|
|
293
|
+
default:
|
|
294
|
+
return 0 /* DEBUG */;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
shouldEnableLogging() {
|
|
298
|
+
if (LOG_LEVEL_ENV === "NONE") return false;
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
shouldUseColors() {
|
|
302
|
+
if (IS_CI) return false;
|
|
303
|
+
if (typeof window !== "undefined") {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (typeof process !== "undefined" && void 0) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return IS_DEVELOPMENT;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var noop = () => {
|
|
313
|
+
};
|
|
314
|
+
var noopWithReturn = () => () => {
|
|
315
|
+
};
|
|
316
|
+
var logger = IS_PRODUCTION ? {
|
|
317
|
+
configure: noop,
|
|
318
|
+
debug: noop,
|
|
319
|
+
// ← This eliminates the call completely
|
|
320
|
+
info: noop,
|
|
321
|
+
// ← This eliminates the call completely
|
|
322
|
+
warn: (message, data) => {
|
|
323
|
+
console.warn(`\u{1F981} LionRapid Core \u26A0\uFE0F ${message}`, data || "");
|
|
324
|
+
},
|
|
325
|
+
error: (message, data) => {
|
|
326
|
+
console.error(`\u{1F981} LionRapid Core \u274C ${message}`, data || "");
|
|
327
|
+
},
|
|
328
|
+
time: noopWithReturn,
|
|
329
|
+
colorize: (text) => text,
|
|
330
|
+
getColors: () => Colors,
|
|
331
|
+
createPackageLogger: (packageName, packageEmoji) => ({
|
|
332
|
+
configure: noop,
|
|
333
|
+
debug: noop,
|
|
334
|
+
// ← Package logger debug also eliminated
|
|
335
|
+
info: noop,
|
|
336
|
+
// ← Package logger info also eliminated
|
|
337
|
+
warn: (message, data) => {
|
|
338
|
+
const prefix = `${packageEmoji || "\u{1F4E6}"} ${packageName}`;
|
|
339
|
+
console.warn(`${prefix} \u26A0\uFE0F ${message}`, data || "");
|
|
340
|
+
},
|
|
341
|
+
error: (message, data) => {
|
|
342
|
+
const prefix = `${packageEmoji || "\u{1F4E6}"} ${packageName}`;
|
|
343
|
+
console.error(`${prefix} \u274C ${message}`, data || "");
|
|
344
|
+
},
|
|
345
|
+
time: noopWithReturn,
|
|
346
|
+
colorize: (text) => text,
|
|
347
|
+
getColors: () => Colors,
|
|
348
|
+
createPackageLogger: noop
|
|
349
|
+
})
|
|
350
|
+
} : new DevelopmentLogger({
|
|
351
|
+
packageName: "LionRapid Core",
|
|
352
|
+
packageEmoji: "\u{1F981}"
|
|
353
|
+
});
|
|
354
|
+
var Logger = IS_PRODUCTION ? function Logger2() {
|
|
355
|
+
return logger;
|
|
356
|
+
} : DevelopmentLogger;
|
|
357
|
+
var createPackageLogger = (config) => {
|
|
358
|
+
if (IS_PRODUCTION) {
|
|
359
|
+
const prefix = `${config.packageEmoji || "\u{1F4E6}"} ${config.packageName || "LionRapid"}`;
|
|
360
|
+
return {
|
|
361
|
+
configure: noop,
|
|
362
|
+
debug: noop,
|
|
363
|
+
// ← All debug calls eliminated
|
|
364
|
+
info: noop,
|
|
365
|
+
// ← All info calls eliminated
|
|
366
|
+
warn: (message, data) => {
|
|
367
|
+
console.warn(`${prefix} \u26A0\uFE0F ${message}`, data || "");
|
|
368
|
+
},
|
|
369
|
+
error: (message, data) => {
|
|
370
|
+
console.error(`${prefix} \u274C ${message}`, data || "");
|
|
371
|
+
},
|
|
372
|
+
time: noopWithReturn,
|
|
373
|
+
colorize: (text) => text,
|
|
374
|
+
getColors: () => Colors,
|
|
375
|
+
createPackageLogger: noop
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return new DevelopmentLogger(config);
|
|
379
|
+
};
|
|
380
|
+
var measure = (name) => {
|
|
381
|
+
if (IS_PRODUCTION) {
|
|
382
|
+
return {
|
|
383
|
+
start: noop,
|
|
384
|
+
end: noop,
|
|
385
|
+
elapsed: () => 0
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
let startTime;
|
|
389
|
+
return {
|
|
390
|
+
start: () => {
|
|
391
|
+
if (IS_DEVELOPMENT) {
|
|
392
|
+
startTime = performance.now();
|
|
393
|
+
logger.debug(`\u23F1\uFE0F ${name} - started`);
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
end: () => {
|
|
397
|
+
if (IS_DEVELOPMENT) {
|
|
398
|
+
const duration = performance.now() - startTime;
|
|
399
|
+
logger.debug(`\u23F1\uFE0F ${name} - completed in ${duration.toFixed(2)}ms`);
|
|
400
|
+
return duration;
|
|
401
|
+
}
|
|
402
|
+
return 0;
|
|
403
|
+
},
|
|
404
|
+
elapsed: () => IS_DEVELOPMENT ? performance.now() - startTime : 0
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// src/base/event-emitter.ts
|
|
409
|
+
var EventEmitter = class {
|
|
410
|
+
constructor() {
|
|
411
|
+
// ✅ Properly typed event listeners without `any`
|
|
412
|
+
__publicField(this, "eventListeners", /* @__PURE__ */ new Map());
|
|
413
|
+
__publicField(this, "debugMode", false);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Subscribe to events (PUBLIC API) - Fully type-safe
|
|
417
|
+
*/
|
|
418
|
+
on(event, listener) {
|
|
419
|
+
if (!this.eventListeners.has(event)) {
|
|
420
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
421
|
+
}
|
|
422
|
+
const listeners = this.eventListeners.get(event);
|
|
423
|
+
listeners.add(listener);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Unsubscribe from events (PUBLIC API) - Fully type-safe
|
|
427
|
+
*/
|
|
428
|
+
off(event, listener) {
|
|
429
|
+
const listeners = this.eventListeners.get(event);
|
|
430
|
+
if (listeners) {
|
|
431
|
+
listeners.delete(listener);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* ✅ Type-safe emit with proper generic constraint
|
|
436
|
+
*/
|
|
437
|
+
emit(event, envelope) {
|
|
438
|
+
const listeners = this.eventListeners.get(event);
|
|
439
|
+
if (listeners) {
|
|
440
|
+
listeners.forEach((listener) => {
|
|
441
|
+
try {
|
|
442
|
+
listener(envelope);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error(`Error in event listener for ${String(event)}:`, error);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
hasListeners(event) {
|
|
450
|
+
return this.eventListeners.has(event) && this.eventListeners.get(event).size > 0;
|
|
451
|
+
}
|
|
452
|
+
clearEvents() {
|
|
453
|
+
this.eventListeners.clear();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* ✅ Create base context once per request
|
|
457
|
+
*/
|
|
458
|
+
createContext(requestId, trace, extra) {
|
|
459
|
+
return {
|
|
460
|
+
requestId,
|
|
461
|
+
timestamp: performance.now(),
|
|
462
|
+
trace,
|
|
463
|
+
...extra
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* ✅ Type-safe event emission helper
|
|
468
|
+
*/
|
|
469
|
+
emitEvent(event, payload, requestId, trace) {
|
|
470
|
+
this.emit(event, {
|
|
471
|
+
context: this.createContext(requestId, trace),
|
|
472
|
+
payload
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* ✅ Translation event builder with proper typing
|
|
477
|
+
*/
|
|
478
|
+
translation(request, handler) {
|
|
479
|
+
const baseTrace = ["translation"];
|
|
480
|
+
const handlerTrace = handler ? [...baseTrace, handler] : baseTrace;
|
|
481
|
+
return {
|
|
482
|
+
request: () => {
|
|
483
|
+
this.emit("translation:request", {
|
|
484
|
+
context: this.createContext(request.id, [...baseTrace, "request"]),
|
|
485
|
+
payload: { key: request.cleanKey }
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
success: (value) => {
|
|
489
|
+
this.emit("translation:success", {
|
|
490
|
+
context: this.createContext(request.id, [...handlerTrace, "success"]),
|
|
491
|
+
payload: { key: request.cleanKey, value }
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
fallback: (value) => {
|
|
495
|
+
this.emit("translation:fallback", {
|
|
496
|
+
context: this.createContext(request.id, [...baseTrace, "fallback"]),
|
|
497
|
+
payload: { key: request.cleanKey, value }
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
missing: (fallbackValue, locale, namespace) => {
|
|
501
|
+
this.emit("translation:missing", {
|
|
502
|
+
context: this.createContext(request.id, [...baseTrace, "missing"]),
|
|
503
|
+
payload: {
|
|
504
|
+
key: request.cleanKey,
|
|
505
|
+
fallbackValue,
|
|
506
|
+
locale,
|
|
507
|
+
namespace
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
notFound: (fallbackValue, locale, namespace) => {
|
|
512
|
+
this.emit("translation:not-found", {
|
|
513
|
+
context: this.createContext(request.id, [...baseTrace, "not-found"]),
|
|
514
|
+
payload: {
|
|
515
|
+
key: request.cleanKey,
|
|
516
|
+
fallbackValue,
|
|
517
|
+
locale,
|
|
518
|
+
namespace,
|
|
519
|
+
status: 404
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
locked: (fallbackValue, locale, namespace) => {
|
|
524
|
+
this.emit("translation:locked", {
|
|
525
|
+
context: this.createContext(request.id, [...baseTrace, "locked"]),
|
|
526
|
+
payload: {
|
|
527
|
+
key: request.cleanKey,
|
|
528
|
+
fallbackValue,
|
|
529
|
+
locale,
|
|
530
|
+
namespace,
|
|
531
|
+
status: 423
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* ✅ Repository event builder with better typing
|
|
539
|
+
*/
|
|
540
|
+
repo(request, source) {
|
|
541
|
+
const baseTrace = ["repo", source];
|
|
542
|
+
return {
|
|
543
|
+
getStart: () => {
|
|
544
|
+
if (typeof request === "string") return;
|
|
545
|
+
this.emit("repo:get:start", {
|
|
546
|
+
context: this.createContext(request.id, [
|
|
547
|
+
...baseTrace,
|
|
548
|
+
"get",
|
|
549
|
+
"start"
|
|
550
|
+
]),
|
|
551
|
+
payload: { key: request.cleanKey, source }
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
getSuccess: (value) => {
|
|
555
|
+
if (typeof request === "string") return;
|
|
556
|
+
this.emit("repo:get:success", {
|
|
557
|
+
context: this.createContext(request.id, [
|
|
558
|
+
...baseTrace,
|
|
559
|
+
"get",
|
|
560
|
+
"success"
|
|
561
|
+
]),
|
|
562
|
+
payload: { key: request.cleanKey, value, source }
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
getMiss: () => {
|
|
566
|
+
if (typeof request === "string") return;
|
|
567
|
+
this.emit("repo:get:miss", {
|
|
568
|
+
context: this.createContext(request.id, [
|
|
569
|
+
...baseTrace,
|
|
570
|
+
"get",
|
|
571
|
+
"miss"
|
|
572
|
+
]),
|
|
573
|
+
payload: { key: request.cleanKey, source }
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
setStart: (value) => {
|
|
577
|
+
if (typeof request === "string") return;
|
|
578
|
+
this.emit("repo:set:start", {
|
|
579
|
+
context: this.createContext(request.id, [
|
|
580
|
+
...baseTrace,
|
|
581
|
+
"set",
|
|
582
|
+
"start"
|
|
583
|
+
]),
|
|
584
|
+
payload: { key: request.cleanKey, value, source }
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
setSuccess: (value) => {
|
|
588
|
+
const id = typeof request === "string" ? "system" : request.id;
|
|
589
|
+
const key = typeof request === "string" ? request : request.cleanKey;
|
|
590
|
+
this.emit("repo:set:success", {
|
|
591
|
+
context: this.createContext(id, [...baseTrace, "set", "success"]),
|
|
592
|
+
payload: { key, value, source }
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
error: (error) => {
|
|
596
|
+
if (typeof request === "string") return;
|
|
597
|
+
this.emit("repo:error", {
|
|
598
|
+
context: this.createContext(request.id, [...baseTrace, "error"]),
|
|
599
|
+
payload: { key: request.cleanKey, source, error }
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* ✅ Network event builder
|
|
606
|
+
*/
|
|
607
|
+
network(request, method) {
|
|
608
|
+
const key = typeof request === "string" ? request : request.cleanKey;
|
|
609
|
+
const requestId = typeof request === "string" ? "network" : request.id;
|
|
610
|
+
const baseTrace = ["network", method || "request"];
|
|
611
|
+
return {
|
|
612
|
+
start: () => {
|
|
613
|
+
this.emit("network:request:start", {
|
|
614
|
+
context: this.createContext(requestId, [...baseTrace, "start"]),
|
|
615
|
+
payload: { key }
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
success: (value) => {
|
|
619
|
+
this.emit("network:request:success", {
|
|
620
|
+
context: this.createContext(requestId, [...baseTrace, "success"]),
|
|
621
|
+
payload: { key, value, method: method || "GET" }
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
error: (error, status) => {
|
|
625
|
+
this.emit("network:request:error", {
|
|
626
|
+
context: this.createContext(requestId, [...baseTrace, "error"]),
|
|
627
|
+
payload: {
|
|
628
|
+
key,
|
|
629
|
+
method: method || "GET",
|
|
630
|
+
error,
|
|
631
|
+
...status && { status }
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
retry: (attempt, delayMs, reason) => {
|
|
636
|
+
this.emit("network:retry", {
|
|
637
|
+
context: this.createContext(requestId, [...baseTrace, "retry"]),
|
|
638
|
+
payload: { key, attempt, delayMs, ...reason && { reason } }
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* ✅ Builder event builder
|
|
645
|
+
*/
|
|
646
|
+
builder() {
|
|
647
|
+
const baseTrace = ["builder"];
|
|
648
|
+
const systemId = `build-${Date.now()}`;
|
|
649
|
+
return {
|
|
650
|
+
use: (type) => {
|
|
651
|
+
this.emit("builder:use", {
|
|
652
|
+
context: this.createContext(systemId, [...baseTrace, "use"]),
|
|
653
|
+
payload: { type }
|
|
654
|
+
});
|
|
655
|
+
},
|
|
656
|
+
buildStart: (locale, namespace) => {
|
|
657
|
+
this.emit("builder:build:start", {
|
|
658
|
+
context: this.createContext(systemId, [
|
|
659
|
+
...baseTrace,
|
|
660
|
+
"build",
|
|
661
|
+
"start"
|
|
662
|
+
]),
|
|
663
|
+
payload: { locale, namespace }
|
|
664
|
+
});
|
|
665
|
+
},
|
|
666
|
+
buildDone: (repositories, plugins) => {
|
|
667
|
+
this.emit("builder:build:done", {
|
|
668
|
+
context: this.createContext(systemId, [
|
|
669
|
+
...baseTrace,
|
|
670
|
+
"build",
|
|
671
|
+
"done"
|
|
672
|
+
]),
|
|
673
|
+
payload: { repositories, plugins }
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* ✅ Language change event builder
|
|
680
|
+
*/
|
|
681
|
+
language(oldLocale, newLocale) {
|
|
682
|
+
const requestId = `lang-change-${Date.now()}`;
|
|
683
|
+
const baseTrace = ["language", "change"];
|
|
684
|
+
return {
|
|
685
|
+
start: () => {
|
|
686
|
+
this.emit("language:change:start", {
|
|
687
|
+
context: this.createContext(requestId, [...baseTrace, "start"]),
|
|
688
|
+
payload: { oldLocale, newLocale }
|
|
689
|
+
});
|
|
690
|
+
},
|
|
691
|
+
complete: () => {
|
|
692
|
+
this.emit("language:change:success", {
|
|
693
|
+
context: this.createContext(requestId, [...baseTrace, "complete"]),
|
|
694
|
+
payload: { oldLocale, newLocale }
|
|
695
|
+
});
|
|
696
|
+
},
|
|
697
|
+
error: (error) => {
|
|
698
|
+
this.emit("language:change:error", {
|
|
699
|
+
context: this.createContext(requestId, [...baseTrace, "error"]),
|
|
700
|
+
payload: { oldLocale, newLocale, error }
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* ✅ Cache operations (debug only)
|
|
707
|
+
*/
|
|
708
|
+
cache(operation, reason, stats) {
|
|
709
|
+
if (!this.debugMode) return { start: () => {
|
|
710
|
+
}, complete: () => {
|
|
711
|
+
} };
|
|
712
|
+
return {
|
|
713
|
+
start: () => {
|
|
714
|
+
logger.debug(`\u{1F9F9} Cache ${operation} starting: ${reason}`);
|
|
715
|
+
},
|
|
716
|
+
complete: () => {
|
|
717
|
+
logger.debug(
|
|
718
|
+
`\u2705 Cache ${operation} complete: ${stats?.clearedCount || 0} entries`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/handlers/handler-chain.ts
|
|
726
|
+
var HandlerChain = class {
|
|
727
|
+
constructor(eventEmitter) {
|
|
728
|
+
__publicField(this, "asyncHandlers", []);
|
|
729
|
+
__publicField(this, "syncHandlers", []);
|
|
730
|
+
__publicField(this, "bus");
|
|
731
|
+
this.bus = eventEmitter;
|
|
732
|
+
}
|
|
733
|
+
addSyncHandler(handler) {
|
|
734
|
+
if (!handler.enabled) return;
|
|
735
|
+
this.syncHandlers.push(handler);
|
|
736
|
+
this.sortHandlersIfNeeded(this.syncHandlers);
|
|
737
|
+
logger.debug(`Sync handler added: ${handler.name}`);
|
|
738
|
+
}
|
|
739
|
+
handleSync(request) {
|
|
740
|
+
for (const handler of this.syncHandlers) {
|
|
741
|
+
try {
|
|
742
|
+
const result = handler.lookup(request);
|
|
743
|
+
if (result !== null && result.value) {
|
|
744
|
+
logger.debug(`Sync hit: ${handler.name} (${result.duration}ms)`);
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
logger.warn(`Sync lookup failed in ${handler.name}:`, error);
|
|
749
|
+
const repoBus = this.bus.repo(request, handler.name);
|
|
750
|
+
repoBus.error(error);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
saveToMemoryHandlers(request, result, _sourceHandler) {
|
|
757
|
+
const memoryHandlers = this.syncHandlers;
|
|
758
|
+
if (memoryHandlers.length === 0) return;
|
|
759
|
+
logger.debug(`Saving to ${memoryHandlers.length} memory handlers`);
|
|
760
|
+
memoryHandlers.forEach((handler) => {
|
|
761
|
+
try {
|
|
762
|
+
handler.save(request, result);
|
|
763
|
+
logger.debug(`Saved to memory: ${handler.name}`);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
logger.warn(`Failed to save to memory ${handler.name}:`, error);
|
|
766
|
+
const repoBus = this.bus.repo(request, handler.name);
|
|
767
|
+
repoBus.error(error);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
saveNamespaceToMemoryHandlers(translations, context) {
|
|
772
|
+
const memoryHandlers = this.syncHandlers.filter(
|
|
773
|
+
(h) => h.type === "memory" && h.saveNamespace
|
|
774
|
+
);
|
|
775
|
+
if (memoryHandlers.length === 0) return;
|
|
776
|
+
logger.debug(
|
|
777
|
+
`Saving namespace to ${memoryHandlers.length} memory handlers`
|
|
778
|
+
);
|
|
779
|
+
memoryHandlers.forEach((handler) => {
|
|
780
|
+
try {
|
|
781
|
+
handler.saveNamespace(translations, context);
|
|
782
|
+
logger.debug(`Namespace saved to memory: ${handler.name}`, {
|
|
783
|
+
locale: context.locale,
|
|
784
|
+
namespace: context.namespace
|
|
785
|
+
});
|
|
786
|
+
} catch (error) {
|
|
787
|
+
logger.warn(
|
|
788
|
+
`Failed to save namespace to memory ${handler.name}:`,
|
|
789
|
+
error
|
|
790
|
+
);
|
|
791
|
+
const repoBus = this.bus.repo(
|
|
792
|
+
`${context.locale}:${context.namespace}`,
|
|
793
|
+
handler.name
|
|
794
|
+
);
|
|
795
|
+
repoBus.error(error);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
addAsyncHandler(handler) {
|
|
800
|
+
if (!handler.enabled) return;
|
|
801
|
+
this.asyncHandlers.push(handler);
|
|
802
|
+
this.sortHandlersIfNeeded(this.asyncHandlers);
|
|
803
|
+
logger.debug(`Async handler added: ${handler.name}`);
|
|
804
|
+
}
|
|
805
|
+
async handleAsync(request) {
|
|
806
|
+
for (const handler of this.asyncHandlers) {
|
|
807
|
+
try {
|
|
808
|
+
logger.debug(`Trying handler: ${handler.name}`);
|
|
809
|
+
const result = await handler.lookup(request);
|
|
810
|
+
if (result.value && result.status === 200) {
|
|
811
|
+
logger.debug(`Async hit: ${handler.name} (${result.duration}ms)`);
|
|
812
|
+
if (result.namespaceLoaded) {
|
|
813
|
+
logger.debug(
|
|
814
|
+
`Saving loaded namespace: ${result.namespaceLoaded.locale}:${result.namespaceLoaded.namespace}`
|
|
815
|
+
);
|
|
816
|
+
await this.saveNamespaceToHandlers(result.namespaceLoaded.data, {
|
|
817
|
+
locale: result.namespaceLoaded.locale,
|
|
818
|
+
namespace: result.namespaceLoaded.namespace
|
|
819
|
+
});
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
await this.saveToStorageHandlers(request, result, handler);
|
|
823
|
+
this.saveToMemoryHandlers(request, result, handler);
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
logger.debug(
|
|
827
|
+
`Handler ${handler.name} returned fallback (status: ${result.status})`
|
|
828
|
+
);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
logger.warn(`Handler ${handler.name} failed:`, error);
|
|
831
|
+
const repoBus = this.bus.repo(request, handler.name);
|
|
832
|
+
repoBus.error(error);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
logger.debug(`No async handlers found translation for ${request.cleanKey}`);
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Save translation to all storage handlers that come before the source handler
|
|
841
|
+
*/
|
|
842
|
+
async saveToStorageHandlers(request, result, sourceHandler) {
|
|
843
|
+
const sourceIndex = this.asyncHandlers.indexOf(sourceHandler);
|
|
844
|
+
const storageHandlers = this.asyncHandlers.slice(0, sourceIndex).filter((h) => h.type === "storage");
|
|
845
|
+
if (storageHandlers.length === 0) return;
|
|
846
|
+
logger.debug(`Saving to ${storageHandlers.length} storage handlers`);
|
|
847
|
+
const savePromises = storageHandlers.map(async (handler) => {
|
|
848
|
+
try {
|
|
849
|
+
await handler.save(request, result);
|
|
850
|
+
logger.debug(`Saved to storage: ${handler.name}`);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
logger.warn(`Failed to save to storage ${handler.name}:`, error);
|
|
853
|
+
const repoBus = this.bus.repo(request, handler.name);
|
|
854
|
+
repoBus.error(error);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
await Promise.allSettled(savePromises);
|
|
858
|
+
}
|
|
859
|
+
async saveNamespaceToStorageHandlers(translations, context) {
|
|
860
|
+
const storageHandlers = this.asyncHandlers.filter(
|
|
861
|
+
(h) => h.type === "storage" && h.saveNamespace
|
|
862
|
+
);
|
|
863
|
+
if (storageHandlers.length === 0) return;
|
|
864
|
+
logger.debug(
|
|
865
|
+
`Saving namespace to ${storageHandlers.length} storage handlers`
|
|
866
|
+
);
|
|
867
|
+
const savePromises = storageHandlers.map(async (handler) => {
|
|
868
|
+
try {
|
|
869
|
+
await handler.saveNamespace(translations, context);
|
|
870
|
+
logger.debug(`Namespace saved to storage: ${handler.name}`, {
|
|
871
|
+
locale: context.locale,
|
|
872
|
+
namespace: context.namespace
|
|
873
|
+
});
|
|
874
|
+
} catch (error) {
|
|
875
|
+
logger.warn(
|
|
876
|
+
`Failed to save namespace to storage ${handler.name}:`,
|
|
877
|
+
error
|
|
878
|
+
);
|
|
879
|
+
const repoBus = this.bus.repo(
|
|
880
|
+
`${context.locale}:${context.namespace}`,
|
|
881
|
+
handler.name
|
|
882
|
+
);
|
|
883
|
+
repoBus.error(error);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
await Promise.allSettled(savePromises);
|
|
887
|
+
}
|
|
888
|
+
async saveNamespaceToHandlers(translations, context) {
|
|
889
|
+
this.saveNamespaceToMemoryHandlers(translations, context);
|
|
890
|
+
await this.saveNamespaceToStorageHandlers(translations, context);
|
|
891
|
+
}
|
|
892
|
+
getSyncHandlers() {
|
|
893
|
+
return [...this.syncHandlers];
|
|
894
|
+
}
|
|
895
|
+
getAsyncHandlers() {
|
|
896
|
+
return [...this.asyncHandlers];
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Get chain info for debugging
|
|
900
|
+
*/
|
|
901
|
+
getChainInfo() {
|
|
902
|
+
return {
|
|
903
|
+
length: this.syncHandlers.length + this.asyncHandlers.length,
|
|
904
|
+
handlers: [
|
|
905
|
+
...this.syncHandlers.map((h) => ({
|
|
906
|
+
name: h.name,
|
|
907
|
+
priority: h.priority,
|
|
908
|
+
type: h.type,
|
|
909
|
+
enabled: h.enabled
|
|
910
|
+
})),
|
|
911
|
+
...this.asyncHandlers.map((h) => ({
|
|
912
|
+
name: h.name,
|
|
913
|
+
priority: h.priority,
|
|
914
|
+
type: h.type,
|
|
915
|
+
enabled: h.enabled
|
|
916
|
+
}))
|
|
917
|
+
]
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Check if any handler has cached data
|
|
922
|
+
*/
|
|
923
|
+
isCached(request) {
|
|
924
|
+
return this.syncHandlers.some((h) => h.isCached?.(request) === true) || this.asyncHandlers.some((h) => h.isCached?.(request) === true);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Get combined cache entries
|
|
928
|
+
*/
|
|
929
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
930
|
+
getEntries() {
|
|
931
|
+
const entries = {};
|
|
932
|
+
const allHandlers = [...this.syncHandlers, ...this.asyncHandlers];
|
|
933
|
+
for (const handler of allHandlers) {
|
|
934
|
+
if (handler.getEntries) {
|
|
935
|
+
entries[handler.name] = handler.getEntries();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return entries;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Destroy all handlers
|
|
942
|
+
*/
|
|
943
|
+
async destroy() {
|
|
944
|
+
const allHandlers = [...this.syncHandlers, ...this.asyncHandlers];
|
|
945
|
+
const destroyPromises = allHandlers.map(async (handler) => {
|
|
946
|
+
try {
|
|
947
|
+
await handler.destroy?.();
|
|
948
|
+
logger.debug(`Handler destroyed: ${handler.name}`);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
logger.warn(`Failed to destroy handler: ${handler.name}`, error);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
await Promise.allSettled(destroyPromises);
|
|
954
|
+
this.syncHandlers = [];
|
|
955
|
+
this.asyncHandlers = [];
|
|
956
|
+
}
|
|
957
|
+
sortHandlersIfNeeded(handlers) {
|
|
958
|
+
const hasExplicitPriorities = handlers.some((h) => h.priority > 0);
|
|
959
|
+
if (hasExplicitPriorities) {
|
|
960
|
+
handlers.sort((a, b) => {
|
|
961
|
+
const priorityA = a.priority || Number.MAX_SAFE_INTEGER;
|
|
962
|
+
const priorityB = b.priority || Number.MAX_SAFE_INTEGER;
|
|
963
|
+
return priorityA - priorityB;
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async loadNamespace(locale, namespace) {
|
|
968
|
+
for (const handler of this.asyncHandlers) {
|
|
969
|
+
if (handler.type !== "network") continue;
|
|
970
|
+
try {
|
|
971
|
+
logger.debug(
|
|
972
|
+
`Trying to load namespace: ${locale}:${namespace} from ${handler.name}`
|
|
973
|
+
);
|
|
974
|
+
const result = await handler.loadNamespace?.(locale, namespace);
|
|
975
|
+
if (result && result.data && result.status === 200) {
|
|
976
|
+
logger.debug(
|
|
977
|
+
`Loaded namespace: ${locale}:${namespace} from ${handler.name}`
|
|
978
|
+
);
|
|
979
|
+
await this.saveNamespaceToStorageHandlers(result.data, {
|
|
980
|
+
namespace,
|
|
981
|
+
locale
|
|
982
|
+
});
|
|
983
|
+
this.saveNamespaceToMemoryHandlers(result.data, {
|
|
984
|
+
namespace,
|
|
985
|
+
locale
|
|
986
|
+
});
|
|
987
|
+
return result.data;
|
|
988
|
+
}
|
|
989
|
+
} catch (error) {
|
|
990
|
+
logger.warn(
|
|
991
|
+
`Failed to load namespace: ${locale}:${namespace} from ${handler.name}`,
|
|
992
|
+
error
|
|
993
|
+
);
|
|
994
|
+
const repoBus = this.bus.repo(`${locale}:${namespace}`, handler.name);
|
|
995
|
+
repoBus.error(error);
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
logger.debug(`No handlers found namespace: ${locale}:${namespace}`);
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
clearCache() {
|
|
1003
|
+
const allHandlers = [...this.syncHandlers, ...this.asyncHandlers];
|
|
1004
|
+
for (const handler of allHandlers) {
|
|
1005
|
+
try {
|
|
1006
|
+
handler.destroy?.();
|
|
1007
|
+
logger.debug(`Cache cleared for handler: ${handler.name}`);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
logger.warn(
|
|
1010
|
+
`Failed to clear cache for handler: ${handler.name}`,
|
|
1011
|
+
error
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
logger.debug("Cache cleared for all handlers");
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Clear cache entries for a specific namespace
|
|
1019
|
+
*/
|
|
1020
|
+
clearNamespace(locale, namespace) {
|
|
1021
|
+
for (const handler of this.syncHandlers) {
|
|
1022
|
+
try {
|
|
1023
|
+
handler.clearNamespace?.(locale, namespace);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
logger.warn(
|
|
1026
|
+
`Failed to clear namespace for handler: ${handler.name}`,
|
|
1027
|
+
error
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
logger.debug(`Cleared namespace: ${locale}:${namespace}`);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Set a value directly in the cache (for optimistic updates)
|
|
1035
|
+
* Bypasses the normal handler flow to directly set values in memory handlers
|
|
1036
|
+
* Stores as ContentUnit (text + styles) for consistency
|
|
1037
|
+
*
|
|
1038
|
+
* @param key - Full cache key (e.g., 'en:app:welcome')
|
|
1039
|
+
* @param value - Translation value
|
|
1040
|
+
* @param styles - Optional styles for indexed tags
|
|
1041
|
+
*/
|
|
1042
|
+
setCache(key, value, styles) {
|
|
1043
|
+
for (const handler of this.syncHandlers) {
|
|
1044
|
+
if (handler.type === "memory" && handler.setCache) {
|
|
1045
|
+
try {
|
|
1046
|
+
handler.setCache(key, value, styles);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
logger.warn(`Failed to set cache in handler: ${handler.name}`, error);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
logger.debug(`Cache set: ${key}`);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get namespace data from cache for ContentUnit style lookup
|
|
1056
|
+
*
|
|
1057
|
+
* Used by t() to retrieve styles for indexed tags at render time.
|
|
1058
|
+
*
|
|
1059
|
+
* @param namespaceCacheKey - Cache key in format @locale:namespace
|
|
1060
|
+
* @returns Parsed namespace data or null if not found
|
|
1061
|
+
*/
|
|
1062
|
+
getNamespaceData(namespaceCacheKey) {
|
|
1063
|
+
for (const handler of this.syncHandlers) {
|
|
1064
|
+
if (handler.type === "memory") {
|
|
1065
|
+
try {
|
|
1066
|
+
const entries = handler.getEntries?.();
|
|
1067
|
+
if (entries && entries[namespaceCacheKey]) {
|
|
1068
|
+
const data = entries[namespaceCacheKey];
|
|
1069
|
+
if (typeof data === "string") {
|
|
1070
|
+
return JSON.parse(data);
|
|
1071
|
+
}
|
|
1072
|
+
return data;
|
|
1073
|
+
}
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
logger.debug(`Error getting namespace data from ${handler.name}:`, error);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/utils/namespace-utils.ts
|
|
1084
|
+
function parseNamespaceKey(key) {
|
|
1085
|
+
const colonIndex = key.indexOf(":");
|
|
1086
|
+
if (colonIndex > 0 && colonIndex < key.length - 1) {
|
|
1087
|
+
const namespace = key.substring(0, colonIndex);
|
|
1088
|
+
const cleanKey = key.substring(colonIndex + 1);
|
|
1089
|
+
return {
|
|
1090
|
+
originalKey: key,
|
|
1091
|
+
namespace,
|
|
1092
|
+
cleanKey,
|
|
1093
|
+
hasExplicitNamespace: true
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
originalKey: key,
|
|
1098
|
+
cleanKey: key,
|
|
1099
|
+
hasExplicitNamespace: false
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/utils/context-utils.ts
|
|
1104
|
+
function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
|
|
1105
|
+
const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
|
|
1106
|
+
return {
|
|
1107
|
+
locale: currentLocale,
|
|
1108
|
+
namespace: effectiveNamespace,
|
|
1109
|
+
fallbackLocale: fallbackLocale || currentLocale
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/utils/translation-value-utils.ts
|
|
1114
|
+
function resolveNestedKey(namespace, keyPath) {
|
|
1115
|
+
if (!namespace || !keyPath) {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
if (!keyPath.includes(".") && !keyPath.includes("[")) {
|
|
1119
|
+
return namespace[keyPath];
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const keys = parseKeyPath(keyPath);
|
|
1123
|
+
let current = namespace;
|
|
1124
|
+
for (const key of keys) {
|
|
1125
|
+
if (current === null || current === void 0) {
|
|
1126
|
+
logger.debug(
|
|
1127
|
+
`\u{1F50D} Nested key resolution stopped at null/undefined for: ${keyPath}`
|
|
1128
|
+
);
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof key === "number") {
|
|
1132
|
+
if (!Array.isArray(current)) {
|
|
1133
|
+
logger.debug(
|
|
1134
|
+
`\u{1F50D} Expected array for index ${key} in path: ${keyPath}`
|
|
1135
|
+
);
|
|
1136
|
+
return void 0;
|
|
1137
|
+
}
|
|
1138
|
+
current = current[key];
|
|
1139
|
+
} else {
|
|
1140
|
+
if (typeof current !== "object") {
|
|
1141
|
+
logger.debug(
|
|
1142
|
+
`\u{1F50D} Expected object for key '${key}' in path: ${keyPath}`
|
|
1143
|
+
);
|
|
1144
|
+
return void 0;
|
|
1145
|
+
}
|
|
1146
|
+
current = current[key];
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
logger.debug(
|
|
1150
|
+
`\u{1F50D} Nested key resolved: ${keyPath} \u2192 ${typeof current === "object" ? "[Object]" : current}`
|
|
1151
|
+
);
|
|
1152
|
+
return current;
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
logger.debug(`\u{1F50D} Nested key resolution error for '${keyPath}':`, error);
|
|
1155
|
+
return void 0;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function parseKeyPath(keyPath) {
|
|
1159
|
+
const keys = [];
|
|
1160
|
+
let currentKey = "";
|
|
1161
|
+
let inBrackets = false;
|
|
1162
|
+
for (let i = 0; i < keyPath.length; i++) {
|
|
1163
|
+
const char = keyPath[i];
|
|
1164
|
+
if (char === "." && !inBrackets) {
|
|
1165
|
+
if (currentKey) {
|
|
1166
|
+
keys.push(currentKey);
|
|
1167
|
+
currentKey = "";
|
|
1168
|
+
}
|
|
1169
|
+
} else if (char === "[") {
|
|
1170
|
+
if (currentKey) {
|
|
1171
|
+
keys.push(currentKey);
|
|
1172
|
+
currentKey = "";
|
|
1173
|
+
}
|
|
1174
|
+
inBrackets = true;
|
|
1175
|
+
} else if (char === "]" && inBrackets) {
|
|
1176
|
+
if (currentKey) {
|
|
1177
|
+
const index = parseInt(currentKey, 10);
|
|
1178
|
+
if (!isNaN(index)) {
|
|
1179
|
+
keys.push(index);
|
|
1180
|
+
} else {
|
|
1181
|
+
keys.push(currentKey);
|
|
1182
|
+
}
|
|
1183
|
+
currentKey = "";
|
|
1184
|
+
}
|
|
1185
|
+
inBrackets = false;
|
|
1186
|
+
} else {
|
|
1187
|
+
currentKey += char;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (currentKey) {
|
|
1191
|
+
keys.push(currentKey);
|
|
1192
|
+
}
|
|
1193
|
+
return keys;
|
|
1194
|
+
}
|
|
1195
|
+
function createIndividualCacheKey(locale, namespace, key) {
|
|
1196
|
+
const parts = [locale];
|
|
1197
|
+
if (namespace) {
|
|
1198
|
+
parts.push(namespace);
|
|
1199
|
+
}
|
|
1200
|
+
parts.push(key);
|
|
1201
|
+
return parts.join(":");
|
|
1202
|
+
}
|
|
1203
|
+
function createNamespaceCacheKey(locale, namespace) {
|
|
1204
|
+
return `@${locale}:${namespace}`;
|
|
1205
|
+
}
|
|
1206
|
+
function flattenTranslations(translations, prefix = "", locale, namespace) {
|
|
1207
|
+
const flattened = {};
|
|
1208
|
+
for (const [key, value] of Object.entries(translations)) {
|
|
1209
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1210
|
+
const cacheKey = createIndividualCacheKey(locale, namespace, fullKey);
|
|
1211
|
+
if (typeof value === "object" && value !== null && "text" in value && "styles" in value) {
|
|
1212
|
+
flattened[cacheKey] = JSON.stringify(value);
|
|
1213
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1214
|
+
Object.assign(
|
|
1215
|
+
flattened,
|
|
1216
|
+
flattenTranslations(value, fullKey, locale, namespace)
|
|
1217
|
+
);
|
|
1218
|
+
} else {
|
|
1219
|
+
flattened[cacheKey] = JSON.stringify({ text: String(value), styles: {} });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return flattened;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/utils/translation-flow.ts
|
|
1226
|
+
function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
|
|
1227
|
+
const startTime = performance.now();
|
|
1228
|
+
const { defaultValue, params } = parseTranslationParams(
|
|
1229
|
+
defaultValueOrParams,
|
|
1230
|
+
paramsOrDefaultValue
|
|
1231
|
+
);
|
|
1232
|
+
const parsedKey = parseNamespaceKey(key);
|
|
1233
|
+
const context = createTranslationContext(
|
|
1234
|
+
parsedKey,
|
|
1235
|
+
currentLocale,
|
|
1236
|
+
defaultNamespace,
|
|
1237
|
+
fallbackLocale
|
|
1238
|
+
);
|
|
1239
|
+
const individualCacheKey = createIndividualCacheKey(
|
|
1240
|
+
context.locale,
|
|
1241
|
+
context.namespace,
|
|
1242
|
+
parsedKey.cleanKey
|
|
1243
|
+
);
|
|
1244
|
+
const namespaceCacheKey = context.namespace ? createNamespaceCacheKey(context.locale, context.namespace) : void 0;
|
|
1245
|
+
const id = `${individualCacheKey}-${startTime}`;
|
|
1246
|
+
return {
|
|
1247
|
+
id,
|
|
1248
|
+
// Unique request ID
|
|
1249
|
+
originalKey: key,
|
|
1250
|
+
originalParams: defaultValueOrParams,
|
|
1251
|
+
originalDefaultValue: paramsOrDefaultValue,
|
|
1252
|
+
cleanKey: parsedKey.cleanKey,
|
|
1253
|
+
hasExplicitNamespace: parsedKey.hasExplicitNamespace,
|
|
1254
|
+
parsedParams: params,
|
|
1255
|
+
parsedDefaultValue: defaultValue,
|
|
1256
|
+
locale: currentLocale,
|
|
1257
|
+
namespace: context.namespace,
|
|
1258
|
+
// ✅ Use effective namespace (explicit or default)
|
|
1259
|
+
fallbackLocale,
|
|
1260
|
+
individualCacheKey,
|
|
1261
|
+
// ✅ Ready-to-use cache key
|
|
1262
|
+
namespaceCacheKey,
|
|
1263
|
+
// ✅ Ready-to-use namespace key
|
|
1264
|
+
startTime
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function parseTranslationParams(secondParam, thirdParam) {
|
|
1268
|
+
let defaultValue;
|
|
1269
|
+
let params;
|
|
1270
|
+
if (typeof secondParam === "object" && typeof thirdParam === "object" && false) {
|
|
1271
|
+
console.warn(
|
|
1272
|
+
"[LionRapid] Warning: Both secondParam and thirdParam are objects. Expected pattern: t(key, params, defaultValue) or t(key, defaultValue, params). Second object will be ignored as it cannot be a defaultValue."
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof secondParam === "string") {
|
|
1276
|
+
defaultValue = secondParam;
|
|
1277
|
+
if (typeof thirdParam === "object") {
|
|
1278
|
+
params = thirdParam;
|
|
1279
|
+
}
|
|
1280
|
+
} else if (typeof secondParam === "object") {
|
|
1281
|
+
params = secondParam;
|
|
1282
|
+
if (typeof thirdParam === "string") {
|
|
1283
|
+
defaultValue = thirdParam;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return { defaultValue, params };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/handlers/sync-storage-handler.ts
|
|
1290
|
+
var SyncHandler = class {
|
|
1291
|
+
constructor(eventEmitter, repository) {
|
|
1292
|
+
__publicField(this, "name");
|
|
1293
|
+
__publicField(this, "version", "1.0.0");
|
|
1294
|
+
__publicField(this, "priority");
|
|
1295
|
+
__publicField(this, "type", "memory");
|
|
1296
|
+
__publicField(this, "enabled");
|
|
1297
|
+
__publicField(this, "ttlMap", /* @__PURE__ */ new Map());
|
|
1298
|
+
__publicField(this, "ttl");
|
|
1299
|
+
__publicField(this, "repository");
|
|
1300
|
+
__publicField(this, "bus");
|
|
1301
|
+
this.bus = eventEmitter;
|
|
1302
|
+
this.repository = repository;
|
|
1303
|
+
this.name = repository.name;
|
|
1304
|
+
const config = repository.getConfig();
|
|
1305
|
+
this.enabled = config.enabled ?? true;
|
|
1306
|
+
this.priority = config.priority ?? 0;
|
|
1307
|
+
this.ttl = config.options?.ttl ?? 30 * 60 * 1e3;
|
|
1308
|
+
}
|
|
1309
|
+
getSourceType() {
|
|
1310
|
+
return "memory";
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Simple lookup implementation
|
|
1314
|
+
* Returns ContentUnit with both text and styles
|
|
1315
|
+
*/
|
|
1316
|
+
lookup(request) {
|
|
1317
|
+
const startTime = Date.now();
|
|
1318
|
+
logger.debug(`Sync lookup: ${request.cleanKey}`);
|
|
1319
|
+
const repoBus = this.bus.repo(request, this.name);
|
|
1320
|
+
repoBus.getStart();
|
|
1321
|
+
const directResult = this.tryDirectLookup(request);
|
|
1322
|
+
if (directResult !== null) {
|
|
1323
|
+
logger.debug(`Cache hit: ${request.cleanKey}`);
|
|
1324
|
+
repoBus.getSuccess(directResult.text);
|
|
1325
|
+
return this.createResult(directResult, "memory", 200, startTime, true);
|
|
1326
|
+
}
|
|
1327
|
+
if (request.cleanKey.includes(".") && request.namespace) {
|
|
1328
|
+
const namespaceResult = this.tryNamespaceLookup(request);
|
|
1329
|
+
if (namespaceResult !== null) {
|
|
1330
|
+
logger.debug(`Namespace hit: ${request.cleanKey}`);
|
|
1331
|
+
repoBus.getSuccess(namespaceResult.text);
|
|
1332
|
+
return this.createResult(
|
|
1333
|
+
namespaceResult,
|
|
1334
|
+
"memory",
|
|
1335
|
+
200,
|
|
1336
|
+
startTime,
|
|
1337
|
+
true
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
logger.debug(`Cache miss: ${request.cleanKey}`);
|
|
1342
|
+
repoBus.getMiss();
|
|
1343
|
+
return this.createResult(
|
|
1344
|
+
{ text: request.parsedDefaultValue || request.cleanKey, styles: {} },
|
|
1345
|
+
"fallback",
|
|
1346
|
+
404,
|
|
1347
|
+
startTime
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Simple save implementation
|
|
1352
|
+
* Stores ContentUnit (text + styles) as JSON
|
|
1353
|
+
*/
|
|
1354
|
+
save(request, result) {
|
|
1355
|
+
if (!result.value) return;
|
|
1356
|
+
const repoBus = this.bus.repo(request, this.name);
|
|
1357
|
+
const contentUnit = {
|
|
1358
|
+
text: result.value,
|
|
1359
|
+
styles: result.styles || {}
|
|
1360
|
+
};
|
|
1361
|
+
this.setWithTTL(request.individualCacheKey, JSON.stringify(contentUnit));
|
|
1362
|
+
logger.debug(`Saved to cache: ${request.cleanKey}`);
|
|
1363
|
+
repoBus.setSuccess(result.value);
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Namespace saving implementation
|
|
1367
|
+
*/
|
|
1368
|
+
saveNamespace(translations, context) {
|
|
1369
|
+
if (!context.namespace) return;
|
|
1370
|
+
const repoBus = this.bus.repo(
|
|
1371
|
+
context.locale + ":" + context.namespace,
|
|
1372
|
+
this.name
|
|
1373
|
+
);
|
|
1374
|
+
const namespaceCacheKey = createNamespaceCacheKey(
|
|
1375
|
+
context.locale,
|
|
1376
|
+
context.namespace
|
|
1377
|
+
);
|
|
1378
|
+
this.setWithTTL(namespaceCacheKey, JSON.stringify(translations));
|
|
1379
|
+
const flattened = flattenTranslations(
|
|
1380
|
+
translations,
|
|
1381
|
+
"",
|
|
1382
|
+
context.locale,
|
|
1383
|
+
context.namespace
|
|
1384
|
+
);
|
|
1385
|
+
for (const [flatKey, flatValue] of Object.entries(flattened)) {
|
|
1386
|
+
this.setWithTTL(flatKey, flatValue);
|
|
1387
|
+
}
|
|
1388
|
+
repoBus.setSuccess(JSON.stringify(translations));
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* ✅ Strategy 1: Direct cache lookup using unified pattern
|
|
1392
|
+
* Returns parsed ContentUnit (text + styles) or null
|
|
1393
|
+
*/
|
|
1394
|
+
tryDirectLookup(request) {
|
|
1395
|
+
if (this.checkAndExpireTTL(request.individualCacheKey)) {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
const cachedValue = this.repository.get(request.individualCacheKey);
|
|
1399
|
+
if (!cachedValue) return null;
|
|
1400
|
+
this.refreshTTL(request.individualCacheKey);
|
|
1401
|
+
try {
|
|
1402
|
+
const parsed = JSON.parse(cachedValue);
|
|
1403
|
+
if (typeof parsed === "object" && "text" in parsed) {
|
|
1404
|
+
return {
|
|
1405
|
+
text: String(parsed.text),
|
|
1406
|
+
styles: parsed.styles || {}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
return { text: cachedValue, styles: {} };
|
|
1410
|
+
} catch {
|
|
1411
|
+
return { text: cachedValue, styles: {} };
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* ✅ Strategy 2: Namespace lookup using unified pattern
|
|
1416
|
+
* Returns parsed ContentUnit (text + styles) or null
|
|
1417
|
+
*/
|
|
1418
|
+
tryNamespaceLookup(request) {
|
|
1419
|
+
if (!request.namespace) return null;
|
|
1420
|
+
const namespaceCacheKey = createNamespaceCacheKey(
|
|
1421
|
+
request.locale,
|
|
1422
|
+
request.namespace
|
|
1423
|
+
);
|
|
1424
|
+
if (this.checkAndExpireTTL(namespaceCacheKey)) {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
const namespaceData = this.repository.get(namespaceCacheKey);
|
|
1428
|
+
if (!namespaceData) return null;
|
|
1429
|
+
this.refreshTTL(namespaceCacheKey);
|
|
1430
|
+
try {
|
|
1431
|
+
const translations = JSON.parse(namespaceData);
|
|
1432
|
+
const value = resolveNestedKey(translations, request.cleanKey);
|
|
1433
|
+
if (value !== null && value !== void 0) {
|
|
1434
|
+
let contentUnit;
|
|
1435
|
+
if (typeof value === "object" && value !== null && "text" in value) {
|
|
1436
|
+
contentUnit = {
|
|
1437
|
+
text: String(value.text),
|
|
1438
|
+
styles: value.styles || {}
|
|
1439
|
+
};
|
|
1440
|
+
} else {
|
|
1441
|
+
contentUnit = { text: String(value), styles: {} };
|
|
1442
|
+
}
|
|
1443
|
+
this.setWithTTL(request.individualCacheKey, JSON.stringify(contentUnit));
|
|
1444
|
+
logger.debug(`Cached from namespace: ${request.cleanKey}`);
|
|
1445
|
+
return contentUnit;
|
|
1446
|
+
}
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
logger.debug(
|
|
1449
|
+
`Invalid JSON in namespace cache: ${namespaceCacheKey}`,
|
|
1450
|
+
error
|
|
1451
|
+
);
|
|
1452
|
+
this.repository.remove(namespaceCacheKey);
|
|
1453
|
+
this.ttlMap.delete(namespaceCacheKey);
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Helper to create consistent TranslationResult objects
|
|
1459
|
+
* Now includes styles from ContentUnit
|
|
1460
|
+
*/
|
|
1461
|
+
createResult(contentUnit, source, status, startTime, cached) {
|
|
1462
|
+
return {
|
|
1463
|
+
value: contentUnit.text,
|
|
1464
|
+
styles: contentUnit.styles,
|
|
1465
|
+
source,
|
|
1466
|
+
status,
|
|
1467
|
+
duration: Date.now() - startTime,
|
|
1468
|
+
...cached !== void 0 && { cached }
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Check TTL and expire if needed
|
|
1473
|
+
* Returns true if expired, false if still valid
|
|
1474
|
+
*/
|
|
1475
|
+
checkAndExpireTTL(cacheKey) {
|
|
1476
|
+
const timestamp = this.ttlMap.get(cacheKey);
|
|
1477
|
+
if (timestamp && Date.now() - timestamp > this.ttl) {
|
|
1478
|
+
this.repository.remove(cacheKey);
|
|
1479
|
+
this.ttlMap.delete(cacheKey);
|
|
1480
|
+
logger.debug(`TTL expired: ${cacheKey}`);
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Set value in repository and update TTL
|
|
1487
|
+
*/
|
|
1488
|
+
setWithTTL(cacheKey, value) {
|
|
1489
|
+
this.repository.set(cacheKey, value);
|
|
1490
|
+
this.ttlMap.set(cacheKey, Date.now());
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Refresh TTL on access
|
|
1494
|
+
*/
|
|
1495
|
+
refreshTTL(cacheKey) {
|
|
1496
|
+
this.ttlMap.set(cacheKey, Date.now());
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Get TTL information for debugging
|
|
1500
|
+
*/
|
|
1501
|
+
getTTLInfo() {
|
|
1502
|
+
const ttlInfo = {};
|
|
1503
|
+
const now = Date.now();
|
|
1504
|
+
for (const [key, timestamp] of this.ttlMap.entries()) {
|
|
1505
|
+
const age = now - timestamp;
|
|
1506
|
+
const remaining = Math.max(0, this.ttl - age);
|
|
1507
|
+
ttlInfo[key] = { timestamp, age, remaining };
|
|
1508
|
+
}
|
|
1509
|
+
return ttlInfo;
|
|
1510
|
+
}
|
|
1511
|
+
getEntries() {
|
|
1512
|
+
if (this.repository.getEntries !== void 0) {
|
|
1513
|
+
return this.repository.getEntries();
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Clear all entries for a specific namespace
|
|
1518
|
+
* Removes namespace cache entry, individual keys, and TTL entries
|
|
1519
|
+
*/
|
|
1520
|
+
clearNamespace(locale, namespace) {
|
|
1521
|
+
const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
|
|
1522
|
+
const keyPrefix = `${locale}:${namespace}:`;
|
|
1523
|
+
this.repository.remove(namespaceCacheKey);
|
|
1524
|
+
this.ttlMap.delete(namespaceCacheKey);
|
|
1525
|
+
const entries = this.repository.getEntries?.();
|
|
1526
|
+
if (entries) {
|
|
1527
|
+
for (const key of Object.keys(entries)) {
|
|
1528
|
+
if (key.startsWith(keyPrefix)) {
|
|
1529
|
+
this.repository.remove(key);
|
|
1530
|
+
this.ttlMap.delete(key);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
logger.debug(`Cleared namespace: ${locale}:${namespace}`);
|
|
1535
|
+
}
|
|
1536
|
+
destroy() {
|
|
1537
|
+
this.repository.clear();
|
|
1538
|
+
this.ttlMap.clear();
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Direct cache set (for optimistic updates)
|
|
1542
|
+
* Bypasses event emission for direct cache manipulation
|
|
1543
|
+
* Stores as ContentUnit JSON (text + styles)
|
|
1544
|
+
*/
|
|
1545
|
+
setCache(key, value, styles) {
|
|
1546
|
+
const contentUnit = {
|
|
1547
|
+
text: value,
|
|
1548
|
+
styles: styles || {}
|
|
1549
|
+
};
|
|
1550
|
+
this.setWithTTL(key, JSON.stringify(contentUnit));
|
|
1551
|
+
logger.debug(`Direct cache set: ${key}`);
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
// src/handlers/async-storage-handler.ts
|
|
1556
|
+
var AsyncHandler = class {
|
|
1557
|
+
constructor(eventEmitter, repository) {
|
|
1558
|
+
__publicField(this, "name");
|
|
1559
|
+
__publicField(this, "version", "1.0.0");
|
|
1560
|
+
__publicField(this, "priority");
|
|
1561
|
+
__publicField(this, "type", "storage");
|
|
1562
|
+
__publicField(this, "enabled");
|
|
1563
|
+
__publicField(this, "ttlMap", /* @__PURE__ */ new Map());
|
|
1564
|
+
__publicField(this, "ttl");
|
|
1565
|
+
__publicField(this, "repository");
|
|
1566
|
+
__publicField(this, "bus");
|
|
1567
|
+
this.bus = eventEmitter;
|
|
1568
|
+
this.repository = repository;
|
|
1569
|
+
this.name = repository.name;
|
|
1570
|
+
const config = repository.getConfig();
|
|
1571
|
+
this.enabled = config.enabled ?? true;
|
|
1572
|
+
this.priority = config.priority ?? 0;
|
|
1573
|
+
this.ttl = config.options?.ttl ?? 30 * 60 * 1e3;
|
|
1574
|
+
}
|
|
1575
|
+
getSourceType() {
|
|
1576
|
+
return "memory";
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Simple lookup implementation
|
|
1580
|
+
* Returns ContentUnit with both text and styles
|
|
1581
|
+
*/
|
|
1582
|
+
async lookup(request) {
|
|
1583
|
+
const startTime = Date.now();
|
|
1584
|
+
logger.debug(`Async lookup: ${request.cleanKey}`);
|
|
1585
|
+
const repoBus = this.bus.repo(request, this.name);
|
|
1586
|
+
const directResult = await this.tryDirectLookup(request);
|
|
1587
|
+
if (directResult !== null) {
|
|
1588
|
+
logger.debug(`Cache hit: ${request.cleanKey}`);
|
|
1589
|
+
repoBus.getSuccess(directResult.text);
|
|
1590
|
+
return this.createResult(directResult, "storage", 200, startTime, true);
|
|
1591
|
+
}
|
|
1592
|
+
if (request.cleanKey.includes(".") && request.namespace) {
|
|
1593
|
+
const namespaceResult = await this.tryNamespaceLookup(request);
|
|
1594
|
+
if (namespaceResult !== null) {
|
|
1595
|
+
logger.debug(`Namespace hit: ${request.cleanKey}`);
|
|
1596
|
+
repoBus.getSuccess(namespaceResult.text);
|
|
1597
|
+
return this.createResult(
|
|
1598
|
+
namespaceResult,
|
|
1599
|
+
"storage",
|
|
1600
|
+
200,
|
|
1601
|
+
startTime,
|
|
1602
|
+
true
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
logger.debug(`Cache miss: ${request.cleanKey}`);
|
|
1607
|
+
repoBus.getMiss();
|
|
1608
|
+
return this.createResult(
|
|
1609
|
+
{ text: request.parsedDefaultValue || request.cleanKey, styles: {} },
|
|
1610
|
+
"fallback",
|
|
1611
|
+
404,
|
|
1612
|
+
startTime
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Simple save implementation
|
|
1617
|
+
* Stores ContentUnit (text + styles) as JSON
|
|
1618
|
+
*/
|
|
1619
|
+
async save(request, result) {
|
|
1620
|
+
if (!result.value) return;
|
|
1621
|
+
const repoBus = this.bus.repo(request, this.name);
|
|
1622
|
+
const contentUnit = {
|
|
1623
|
+
text: result.value,
|
|
1624
|
+
styles: result.styles || {}
|
|
1625
|
+
};
|
|
1626
|
+
await this.setWithTTL(request.individualCacheKey, JSON.stringify(contentUnit));
|
|
1627
|
+
logger.debug(`Saved to cache: ${request.cleanKey}`);
|
|
1628
|
+
repoBus.setSuccess(result.value);
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Namespace saving implementation
|
|
1632
|
+
*/
|
|
1633
|
+
async saveNamespace(translations, context) {
|
|
1634
|
+
if (!context.namespace) return;
|
|
1635
|
+
const repoBus = this.bus.repo(
|
|
1636
|
+
context.locale + ":" + context.namespace,
|
|
1637
|
+
this.name
|
|
1638
|
+
);
|
|
1639
|
+
const keyCount = Object.keys(translations).length;
|
|
1640
|
+
logger.debug(
|
|
1641
|
+
`Saving namespace: ${context.locale}:${context.namespace} (${keyCount} keys)`
|
|
1642
|
+
);
|
|
1643
|
+
const namespaceCacheKey = createNamespaceCacheKey(
|
|
1644
|
+
context.locale,
|
|
1645
|
+
context.namespace
|
|
1646
|
+
);
|
|
1647
|
+
await this.setWithTTL(namespaceCacheKey, JSON.stringify(translations));
|
|
1648
|
+
const flattened = flattenTranslations(
|
|
1649
|
+
translations,
|
|
1650
|
+
"",
|
|
1651
|
+
context.locale,
|
|
1652
|
+
context.namespace
|
|
1653
|
+
);
|
|
1654
|
+
let savedCount = 0;
|
|
1655
|
+
for (const [flatKey, flatValue] of Object.entries(flattened)) {
|
|
1656
|
+
await this.setWithTTL(flatKey, flatValue);
|
|
1657
|
+
savedCount++;
|
|
1658
|
+
}
|
|
1659
|
+
logger.debug(`Saved: ${savedCount} keys + namespace blob`);
|
|
1660
|
+
repoBus.setSuccess(JSON.stringify(translations));
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* ✅ Strategy 1: Direct cache lookup using unified pattern
|
|
1664
|
+
* Returns parsed ContentUnit (text + styles) or null
|
|
1665
|
+
*/
|
|
1666
|
+
async tryDirectLookup(request) {
|
|
1667
|
+
if (await this.checkAndExpireTTL(request.individualCacheKey)) {
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
const cachedValue = await this.repository.get(request.individualCacheKey);
|
|
1671
|
+
if (!cachedValue) return null;
|
|
1672
|
+
this.refreshTTL(request.individualCacheKey);
|
|
1673
|
+
try {
|
|
1674
|
+
const parsed = JSON.parse(cachedValue);
|
|
1675
|
+
if (typeof parsed === "object" && "text" in parsed) {
|
|
1676
|
+
return {
|
|
1677
|
+
text: String(parsed.text),
|
|
1678
|
+
styles: parsed.styles || {}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
return { text: cachedValue, styles: {} };
|
|
1682
|
+
} catch {
|
|
1683
|
+
return { text: cachedValue, styles: {} };
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* ✅ Strategy 2: Namespace lookup using unified pattern
|
|
1688
|
+
* Returns parsed ContentUnit (text + styles) or null
|
|
1689
|
+
*/
|
|
1690
|
+
async tryNamespaceLookup(request) {
|
|
1691
|
+
if (!request.namespace) return null;
|
|
1692
|
+
const namespaceCacheKey = createNamespaceCacheKey(
|
|
1693
|
+
request.locale,
|
|
1694
|
+
request.namespace
|
|
1695
|
+
);
|
|
1696
|
+
if (await this.checkAndExpireTTL(namespaceCacheKey)) {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
const namespaceData = await this.repository.get(namespaceCacheKey);
|
|
1700
|
+
if (!namespaceData) return null;
|
|
1701
|
+
this.refreshTTL(namespaceCacheKey);
|
|
1702
|
+
try {
|
|
1703
|
+
const translations = JSON.parse(namespaceData);
|
|
1704
|
+
const value = resolveNestedKey(translations, request.cleanKey);
|
|
1705
|
+
if (value !== null && value !== void 0) {
|
|
1706
|
+
let contentUnit;
|
|
1707
|
+
if (typeof value === "object" && value !== null && "text" in value) {
|
|
1708
|
+
contentUnit = {
|
|
1709
|
+
text: String(value.text),
|
|
1710
|
+
styles: value.styles || {}
|
|
1711
|
+
};
|
|
1712
|
+
} else {
|
|
1713
|
+
contentUnit = { text: String(value), styles: {} };
|
|
1714
|
+
}
|
|
1715
|
+
await this.setWithTTL(request.individualCacheKey, JSON.stringify(contentUnit));
|
|
1716
|
+
logger.debug(`Cached from namespace: ${request.cleanKey}`);
|
|
1717
|
+
return contentUnit;
|
|
1718
|
+
}
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
logger.debug(
|
|
1721
|
+
`Invalid JSON in namespace cache: ${namespaceCacheKey}`,
|
|
1722
|
+
error
|
|
1723
|
+
);
|
|
1724
|
+
await this.repository.remove(namespaceCacheKey);
|
|
1725
|
+
this.ttlMap.delete(namespaceCacheKey);
|
|
1726
|
+
}
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Helper to create consistent TranslationResult objects
|
|
1731
|
+
* Now includes styles from ContentUnit
|
|
1732
|
+
*/
|
|
1733
|
+
createResult(contentUnit, source, status, startTime, cached) {
|
|
1734
|
+
return {
|
|
1735
|
+
value: contentUnit.text,
|
|
1736
|
+
styles: contentUnit.styles,
|
|
1737
|
+
source,
|
|
1738
|
+
status,
|
|
1739
|
+
duration: Date.now() - startTime,
|
|
1740
|
+
...cached !== void 0 && { cached }
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Check TTL and expire if needed (async version)
|
|
1745
|
+
* Returns true if expired, false if still valid
|
|
1746
|
+
*/
|
|
1747
|
+
async checkAndExpireTTL(cacheKey) {
|
|
1748
|
+
const timestamp = this.ttlMap.get(cacheKey);
|
|
1749
|
+
if (timestamp && Date.now() - timestamp > this.ttl) {
|
|
1750
|
+
await this.repository.remove(cacheKey);
|
|
1751
|
+
this.ttlMap.delete(cacheKey);
|
|
1752
|
+
logger.debug(`TTL expired: ${cacheKey}`);
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Set value in repository and update TTL (async version)
|
|
1759
|
+
*/
|
|
1760
|
+
async setWithTTL(cacheKey, value) {
|
|
1761
|
+
await this.repository.set(cacheKey, value);
|
|
1762
|
+
this.ttlMap.set(cacheKey, Date.now());
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Refresh TTL on access
|
|
1766
|
+
*/
|
|
1767
|
+
refreshTTL(cacheKey) {
|
|
1768
|
+
this.ttlMap.set(cacheKey, Date.now());
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Get TTL information for debugging
|
|
1772
|
+
*/
|
|
1773
|
+
getTTLInfo() {
|
|
1774
|
+
const ttlInfo = {};
|
|
1775
|
+
const now = Date.now();
|
|
1776
|
+
for (const [key, timestamp] of this.ttlMap.entries()) {
|
|
1777
|
+
const age = now - timestamp;
|
|
1778
|
+
const remaining = Math.max(0, this.ttl - age);
|
|
1779
|
+
ttlInfo[key] = { timestamp, age, remaining };
|
|
1780
|
+
}
|
|
1781
|
+
return ttlInfo;
|
|
1782
|
+
}
|
|
1783
|
+
async destroy() {
|
|
1784
|
+
await this.repository.clear();
|
|
1785
|
+
this.ttlMap.clear();
|
|
1786
|
+
}
|
|
1787
|
+
getEntries() {
|
|
1788
|
+
if (this.repository.getEntries !== void 0) {
|
|
1789
|
+
return this.repository.getEntries();
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
// src/errors/translation-errors.ts
|
|
1795
|
+
var TranslationError = class extends Error {
|
|
1796
|
+
constructor(key, status, message) {
|
|
1797
|
+
super(message);
|
|
1798
|
+
this.key = key;
|
|
1799
|
+
this.status = status;
|
|
1800
|
+
this.name = this.constructor.name;
|
|
1801
|
+
if (Error.captureStackTrace) {
|
|
1802
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
var TranslationNotFoundError = class extends TranslationError {
|
|
1807
|
+
constructor(key) {
|
|
1808
|
+
super(key, 404, `Translation not found: ${key}`);
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
var TranslationLockedError = class extends TranslationError {
|
|
1812
|
+
constructor(key, retryAfter) {
|
|
1813
|
+
super(key, 423, `Translation locked: ${key}`);
|
|
1814
|
+
this.retryAfter = retryAfter;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
var ServerError = class extends TranslationError {
|
|
1818
|
+
constructor(key, status) {
|
|
1819
|
+
super(key, status, `Server error ${status}: ${key}`);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
var NetworkError = class extends Error {
|
|
1823
|
+
constructor(message, cause) {
|
|
1824
|
+
super(message);
|
|
1825
|
+
this.cause = cause;
|
|
1826
|
+
this.name = "NetworkError";
|
|
1827
|
+
if (Error.captureStackTrace) {
|
|
1828
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
function isTranslationError(error) {
|
|
1833
|
+
return error instanceof TranslationError;
|
|
1834
|
+
}
|
|
1835
|
+
function isRetryableError(error) {
|
|
1836
|
+
return error instanceof ServerError || error instanceof NetworkError;
|
|
1837
|
+
}
|
|
1838
|
+
function isClientError(error) {
|
|
1839
|
+
return error instanceof TranslationNotFoundError || error instanceof TranslationLockedError;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/handlers/network-handler.ts
|
|
1843
|
+
var NetworkHandler = class {
|
|
1844
|
+
constructor(eventEmitter, repository) {
|
|
1845
|
+
__publicField(this, "repository");
|
|
1846
|
+
__publicField(this, "name");
|
|
1847
|
+
__publicField(this, "priority");
|
|
1848
|
+
__publicField(this, "version", "1.0.0");
|
|
1849
|
+
__publicField(this, "type", "network");
|
|
1850
|
+
__publicField(this, "enabled");
|
|
1851
|
+
__publicField(this, "bus");
|
|
1852
|
+
__publicField(this, "retryCount");
|
|
1853
|
+
__publicField(this, "retryDelay");
|
|
1854
|
+
this.bus = eventEmitter;
|
|
1855
|
+
this.repository = repository;
|
|
1856
|
+
this.name = repository.name;
|
|
1857
|
+
const config = repository.getConfig();
|
|
1858
|
+
this.enabled = config.enabled ?? false;
|
|
1859
|
+
this.priority = config.priority ?? 0;
|
|
1860
|
+
this.retryCount = config.options?.retries ?? 3;
|
|
1861
|
+
this.retryDelay = config.options?.retryDelay ?? 1e3;
|
|
1862
|
+
}
|
|
1863
|
+
initialize() {
|
|
1864
|
+
return Promise.resolve();
|
|
1865
|
+
}
|
|
1866
|
+
getSourceType() {
|
|
1867
|
+
return "network";
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* ✅ Lookup translation with full error handling
|
|
1871
|
+
* Strategy: Key first, namespace as fallback
|
|
1872
|
+
* Returns TranslationResult with status, source, duration, styles, and optional error
|
|
1873
|
+
*/
|
|
1874
|
+
async lookup(request) {
|
|
1875
|
+
const startTime = Date.now();
|
|
1876
|
+
const fallbackValue = request.parsedDefaultValue || request.cleanKey;
|
|
1877
|
+
if (!this.enabled) {
|
|
1878
|
+
return this.createTranslationResult(
|
|
1879
|
+
fallbackValue,
|
|
1880
|
+
"fallback",
|
|
1881
|
+
0,
|
|
1882
|
+
startTime
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
const networkBus = this.bus.network(request, this.name);
|
|
1886
|
+
networkBus.start();
|
|
1887
|
+
logger.debug(`Network lookup: ${request.cleanKey}`);
|
|
1888
|
+
try {
|
|
1889
|
+
const contentUnit = await this.lookupContentUnitWithRetry(
|
|
1890
|
+
request.individualCacheKey
|
|
1891
|
+
);
|
|
1892
|
+
logger.debug(`Network hit: ${request.cleanKey}`);
|
|
1893
|
+
networkBus.success(contentUnit.text);
|
|
1894
|
+
return this.createTranslationResult(
|
|
1895
|
+
contentUnit.text,
|
|
1896
|
+
"network",
|
|
1897
|
+
200,
|
|
1898
|
+
startTime,
|
|
1899
|
+
void 0,
|
|
1900
|
+
false,
|
|
1901
|
+
void 0,
|
|
1902
|
+
contentUnit.styles
|
|
1903
|
+
);
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
if (error instanceof TranslationNotFoundError && request.namespace) {
|
|
1906
|
+
logger.debug(
|
|
1907
|
+
`Individual key not found, trying namespace: ${request.cleanKey}`
|
|
1908
|
+
);
|
|
1909
|
+
return this.lookupFromNamespace(request, fallbackValue, startTime, networkBus);
|
|
1910
|
+
}
|
|
1911
|
+
if (error instanceof TranslationLockedError) {
|
|
1912
|
+
logger.debug(`Translation locked (423): ${request.cleanKey}`);
|
|
1913
|
+
return this.handleTranslationLocked(
|
|
1914
|
+
request,
|
|
1915
|
+
fallbackValue,
|
|
1916
|
+
error,
|
|
1917
|
+
startTime,
|
|
1918
|
+
networkBus
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
logger.debug(`Network error: ${request.cleanKey}`, error);
|
|
1922
|
+
networkBus.error(error);
|
|
1923
|
+
return this.createTranslationResult(
|
|
1924
|
+
fallbackValue,
|
|
1925
|
+
"fallback",
|
|
1926
|
+
0,
|
|
1927
|
+
startTime,
|
|
1928
|
+
error
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Strategy 2: Namespace fallback when individual key not found
|
|
1934
|
+
*/
|
|
1935
|
+
async lookupFromNamespace(request, fallbackValue, startTime, networkBus) {
|
|
1936
|
+
try {
|
|
1937
|
+
const namespaceResult = await this.loadNamespace(
|
|
1938
|
+
request.locale,
|
|
1939
|
+
request.namespace
|
|
1940
|
+
);
|
|
1941
|
+
if (namespaceResult && namespaceResult.data) {
|
|
1942
|
+
const nestedValue = resolveNestedKey(
|
|
1943
|
+
namespaceResult.data,
|
|
1944
|
+
request.cleanKey
|
|
1945
|
+
);
|
|
1946
|
+
if (nestedValue !== void 0 && nestedValue !== null) {
|
|
1947
|
+
let resolvedValue;
|
|
1948
|
+
let styles = {};
|
|
1949
|
+
if (typeof nestedValue === "object" && nestedValue !== null && "text" in nestedValue) {
|
|
1950
|
+
const contentUnit = nestedValue;
|
|
1951
|
+
resolvedValue = String(contentUnit.text);
|
|
1952
|
+
styles = contentUnit.styles || {};
|
|
1953
|
+
} else {
|
|
1954
|
+
resolvedValue = String(nestedValue);
|
|
1955
|
+
}
|
|
1956
|
+
logger.debug(`Network namespace hit: ${request.cleanKey}, hasStyles=${Object.keys(styles).length > 0}`);
|
|
1957
|
+
networkBus.success(resolvedValue);
|
|
1958
|
+
return this.createTranslationResult(
|
|
1959
|
+
resolvedValue,
|
|
1960
|
+
"network",
|
|
1961
|
+
200,
|
|
1962
|
+
startTime,
|
|
1963
|
+
void 0,
|
|
1964
|
+
false,
|
|
1965
|
+
{
|
|
1966
|
+
locale: request.locale,
|
|
1967
|
+
namespace: request.namespace,
|
|
1968
|
+
data: namespaceResult.data
|
|
1969
|
+
},
|
|
1970
|
+
styles
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const error = new TranslationNotFoundError(request.cleanKey);
|
|
1975
|
+
return this.handleTranslationNotFound(
|
|
1976
|
+
request,
|
|
1977
|
+
fallbackValue,
|
|
1978
|
+
error,
|
|
1979
|
+
startTime
|
|
1980
|
+
);
|
|
1981
|
+
} catch (nsError) {
|
|
1982
|
+
logger.debug(`Namespace load failed: ${request.cleanKey}`, nsError);
|
|
1983
|
+
if (nsError instanceof TranslationNotFoundError) {
|
|
1984
|
+
return this.handleTranslationNotFound(
|
|
1985
|
+
request,
|
|
1986
|
+
fallbackValue,
|
|
1987
|
+
nsError,
|
|
1988
|
+
startTime
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (nsError instanceof TranslationLockedError) {
|
|
1992
|
+
return this.handleTranslationLocked(
|
|
1993
|
+
request,
|
|
1994
|
+
fallbackValue,
|
|
1995
|
+
nsError,
|
|
1996
|
+
startTime,
|
|
1997
|
+
networkBus
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
networkBus.error(nsError);
|
|
2001
|
+
return this.createTranslationResult(
|
|
2002
|
+
fallbackValue,
|
|
2003
|
+
"fallback",
|
|
2004
|
+
0,
|
|
2005
|
+
startTime,
|
|
2006
|
+
nsError
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* ✅ Load entire namespace (used for preloading)
|
|
2012
|
+
* Returns NamespaceResult with status and error information
|
|
2013
|
+
*/
|
|
2014
|
+
async loadNamespace(locale, namespace) {
|
|
2015
|
+
const startTime = Date.now();
|
|
2016
|
+
if (!this.enabled) {
|
|
2017
|
+
return this.createNamespaceResult(null, "fallback", 0, startTime);
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
const data = await this.loadNamespaceWithRetry(locale, namespace);
|
|
2021
|
+
if (data === null) {
|
|
2022
|
+
logger.debug(`Network namespace not modified: ${locale}:${namespace}`);
|
|
2023
|
+
return this.createNamespaceResult(null, "network", 304, startTime);
|
|
2024
|
+
}
|
|
2025
|
+
logger.debug(`Network namespace loaded: ${locale}:${namespace}`);
|
|
2026
|
+
return this.createNamespaceResult(data, "network", 200, startTime);
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
if (error instanceof TranslationLockedError) {
|
|
2029
|
+
logger.debug(`Namespace locked (423): ${locale}:${namespace}`);
|
|
2030
|
+
return this.createNamespaceResult(
|
|
2031
|
+
null,
|
|
2032
|
+
"fallback",
|
|
2033
|
+
423,
|
|
2034
|
+
startTime,
|
|
2035
|
+
error
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
if (error instanceof TranslationNotFoundError) {
|
|
2039
|
+
logger.debug(`Network namespace not found: ${locale}:${namespace}`);
|
|
2040
|
+
return this.createNamespaceResult(
|
|
2041
|
+
null,
|
|
2042
|
+
"fallback",
|
|
2043
|
+
404,
|
|
2044
|
+
startTime,
|
|
2045
|
+
error
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
logger.debug(`Network namespace error: ${locale}:${namespace}`, error);
|
|
2049
|
+
return this.createNamespaceResult(
|
|
2050
|
+
null,
|
|
2051
|
+
"fallback",
|
|
2052
|
+
0,
|
|
2053
|
+
startTime,
|
|
2054
|
+
error
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* ✅ Namespace lookup with retry logic
|
|
2060
|
+
* Throws custom errors (TranslationNotFoundError, TranslationLockedError, etc.)
|
|
2061
|
+
*/
|
|
2062
|
+
async loadNamespaceWithRetry(locale, namespace) {
|
|
2063
|
+
return this.withRetry(
|
|
2064
|
+
() => this.repository.getNamespace(locale, namespace),
|
|
2065
|
+
`${locale}:${namespace}`,
|
|
2066
|
+
"namespace request"
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Simple save implementation (API usually doesn't save)
|
|
2071
|
+
*/
|
|
2072
|
+
async save(request, result) {
|
|
2073
|
+
void request;
|
|
2074
|
+
void result;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* ✅ Lookup with retry logic for individual keys
|
|
2078
|
+
* Throws custom errors (TranslationNotFoundError, TranslationLockedError, etc.)
|
|
2079
|
+
*/
|
|
2080
|
+
async lookupWithRetry(cacheKey) {
|
|
2081
|
+
return this.withRetry(
|
|
2082
|
+
() => this.repository.get(cacheKey),
|
|
2083
|
+
cacheKey,
|
|
2084
|
+
"network request"
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* ✅ Lookup ContentUnit with retry logic for individual keys
|
|
2089
|
+
* Returns full ContentUnit with text and styles
|
|
2090
|
+
* Throws custom errors (TranslationNotFoundError, TranslationLockedError, etc.)
|
|
2091
|
+
*/
|
|
2092
|
+
async lookupContentUnitWithRetry(cacheKey) {
|
|
2093
|
+
const repo = this.repository;
|
|
2094
|
+
if (typeof repo.getContentUnit !== "function") {
|
|
2095
|
+
const text = await this.lookupWithRetry(cacheKey);
|
|
2096
|
+
return { text, styles: {} };
|
|
2097
|
+
}
|
|
2098
|
+
return this.withRetry(
|
|
2099
|
+
() => repo.getContentUnit(cacheKey),
|
|
2100
|
+
cacheKey,
|
|
2101
|
+
"network content unit request"
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Delay utility for retries
|
|
2106
|
+
*/
|
|
2107
|
+
delay(ms) {
|
|
2108
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2109
|
+
}
|
|
2110
|
+
calculateBackoff(attempt) {
|
|
2111
|
+
const baseDelay = this.retryDelay;
|
|
2112
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
2113
|
+
const maxDelay = 1e4;
|
|
2114
|
+
const jitter = Math.random() * 0.1 * exponentialDelay;
|
|
2115
|
+
return Math.min(exponentialDelay + jitter, maxDelay);
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Generic retry wrapper for repository operations
|
|
2119
|
+
* Handles retryable errors with exponential backoff
|
|
2120
|
+
*/
|
|
2121
|
+
async withRetry(operation, contextKey, contextDescription, attempt = 1) {
|
|
2122
|
+
const networkBus = this.bus.network(contextKey, "withRetry");
|
|
2123
|
+
try {
|
|
2124
|
+
return await operation();
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
if (attempt <= this.retryCount && isRetryableError(error)) {
|
|
2127
|
+
const delay = this.calculateBackoff(attempt);
|
|
2128
|
+
networkBus.retry(attempt, delay, error.message);
|
|
2129
|
+
await this.delay(delay);
|
|
2130
|
+
logger.debug(
|
|
2131
|
+
`Retrying ${contextDescription} (${attempt}/${this.retryCount}) after ${delay}ms`
|
|
2132
|
+
);
|
|
2133
|
+
return this.withRetry(
|
|
2134
|
+
operation,
|
|
2135
|
+
contextKey,
|
|
2136
|
+
contextDescription,
|
|
2137
|
+
attempt + 1
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
throw error;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Handle translation not found (after namespace fallback also failed)
|
|
2145
|
+
*/
|
|
2146
|
+
handleTranslationNotFound(request, fallbackValue, error, startTime) {
|
|
2147
|
+
logger.debug(`Network namespace miss: ${request.cleanKey}`);
|
|
2148
|
+
const translateBus = this.bus.translation(request);
|
|
2149
|
+
translateBus.notFound(
|
|
2150
|
+
fallbackValue,
|
|
2151
|
+
request.locale,
|
|
2152
|
+
request.namespace || "default"
|
|
2153
|
+
);
|
|
2154
|
+
return this.createTranslationResult(
|
|
2155
|
+
fallbackValue,
|
|
2156
|
+
"fallback",
|
|
2157
|
+
404,
|
|
2158
|
+
startTime,
|
|
2159
|
+
error
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Handle TranslationLockedError for translation lookups
|
|
2164
|
+
*/
|
|
2165
|
+
handleTranslationLocked(request, fallbackValue, error, startTime, networkBus) {
|
|
2166
|
+
const translateBus = this.bus.translation(request);
|
|
2167
|
+
translateBus.locked(
|
|
2168
|
+
fallbackValue,
|
|
2169
|
+
request.locale,
|
|
2170
|
+
request.namespace || "default"
|
|
2171
|
+
);
|
|
2172
|
+
networkBus.error(error, 423);
|
|
2173
|
+
return this.createTranslationResult(
|
|
2174
|
+
fallbackValue,
|
|
2175
|
+
"fallback",
|
|
2176
|
+
423,
|
|
2177
|
+
startTime,
|
|
2178
|
+
error
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Helper to create consistent TranslationResult objects
|
|
2183
|
+
*/
|
|
2184
|
+
createTranslationResult(value, source, status, startTime, error, cached, namespaceLoaded, styles) {
|
|
2185
|
+
return {
|
|
2186
|
+
value,
|
|
2187
|
+
source,
|
|
2188
|
+
status,
|
|
2189
|
+
duration: Date.now() - startTime,
|
|
2190
|
+
...error && { error },
|
|
2191
|
+
...cached !== void 0 && { cached },
|
|
2192
|
+
...namespaceLoaded && { namespaceLoaded },
|
|
2193
|
+
...styles && Object.keys(styles).length > 0 && { styles }
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Helper to create consistent NamespaceResult objects
|
|
2198
|
+
*/
|
|
2199
|
+
createNamespaceResult(data, source, status, startTime, error) {
|
|
2200
|
+
return {
|
|
2201
|
+
data,
|
|
2202
|
+
source,
|
|
2203
|
+
status,
|
|
2204
|
+
duration: Date.now() - startTime,
|
|
2205
|
+
...error && { error }
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
getPreload() {
|
|
2209
|
+
const config = this.repository.getConfig();
|
|
2210
|
+
return config.options?.preloadNamespaces || [];
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
// src/content/style-mapper/DefaultStyleMapper.ts
|
|
2215
|
+
var DefaultStyleMapper = class {
|
|
2216
|
+
constructor() {
|
|
2217
|
+
/**
|
|
2218
|
+
* Element → Style mapping
|
|
2219
|
+
* Maps HTML element names to their style property
|
|
2220
|
+
*/
|
|
2221
|
+
__publicField(this, "elementToStyles", {
|
|
2222
|
+
b: "bold",
|
|
2223
|
+
strong: "bold",
|
|
2224
|
+
i: "italic",
|
|
2225
|
+
em: "italic",
|
|
2226
|
+
u: "underline",
|
|
2227
|
+
s: "strikethrough",
|
|
2228
|
+
del: "strikethrough",
|
|
2229
|
+
strike: "strikethrough",
|
|
2230
|
+
code: "code",
|
|
2231
|
+
mark: "highlight",
|
|
2232
|
+
sub: "subscript",
|
|
2233
|
+
sup: "superscript"
|
|
2234
|
+
});
|
|
2235
|
+
/**
|
|
2236
|
+
* Style → Element mapping (reverse)
|
|
2237
|
+
* Uses simple, familiar elements for developer convenience
|
|
2238
|
+
* Also accepts element names directly (strong, em, etc.)
|
|
2239
|
+
*/
|
|
2240
|
+
__publicField(this, "styleToElement", {
|
|
2241
|
+
// Semantic style names → simple elements
|
|
2242
|
+
bold: "b",
|
|
2243
|
+
italic: "i",
|
|
2244
|
+
underline: "u",
|
|
2245
|
+
strikethrough: "s",
|
|
2246
|
+
code: "code",
|
|
2247
|
+
highlight: "mark",
|
|
2248
|
+
subscript: "sub",
|
|
2249
|
+
superscript: "sup",
|
|
2250
|
+
// Element names (for servers that store styles as element names)
|
|
2251
|
+
strong: "b",
|
|
2252
|
+
b: "b",
|
|
2253
|
+
em: "i",
|
|
2254
|
+
i: "i",
|
|
2255
|
+
u: "u",
|
|
2256
|
+
s: "s",
|
|
2257
|
+
del: "s",
|
|
2258
|
+
strike: "s",
|
|
2259
|
+
mark: "mark",
|
|
2260
|
+
sub: "sub",
|
|
2261
|
+
sup: "sup"
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Convert HTML element to style properties
|
|
2266
|
+
*
|
|
2267
|
+
* @param element - Tag name (e.g., "b", "a", "strong")
|
|
2268
|
+
* @param attrs - Element attributes (e.g., { href: "/url" })
|
|
2269
|
+
* @returns Array of style strings
|
|
2270
|
+
*/
|
|
2271
|
+
mapElementToStyles(element, attrs) {
|
|
2272
|
+
const styles = [];
|
|
2273
|
+
const tag = element.toLowerCase();
|
|
2274
|
+
const mappedStyle = this.elementToStyles[tag];
|
|
2275
|
+
if (mappedStyle) {
|
|
2276
|
+
styles.push(mappedStyle);
|
|
2277
|
+
}
|
|
2278
|
+
if (tag === "a" && attrs.href) {
|
|
2279
|
+
styles.push(`hyperlink:${attrs.href}`);
|
|
2280
|
+
}
|
|
2281
|
+
return styles;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Convert style properties to HTML element
|
|
2285
|
+
*
|
|
2286
|
+
* Priority order:
|
|
2287
|
+
* 1. Hyperlink (becomes wrapper element with href)
|
|
2288
|
+
* 2. First recognized style property
|
|
2289
|
+
* 3. Fallback to span
|
|
2290
|
+
*
|
|
2291
|
+
* @param styles - Array of style strings
|
|
2292
|
+
* @returns Element info for serialization
|
|
2293
|
+
*/
|
|
2294
|
+
mapStylesToElement(styles) {
|
|
2295
|
+
for (const style of styles) {
|
|
2296
|
+
if (style.startsWith("hyperlink:")) {
|
|
2297
|
+
return {
|
|
2298
|
+
element: "a",
|
|
2299
|
+
attrs: { href: style.slice("hyperlink:".length) }
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
for (const style of styles) {
|
|
2304
|
+
const element = this.styleToElement[style];
|
|
2305
|
+
if (element) {
|
|
2306
|
+
return {
|
|
2307
|
+
element,
|
|
2308
|
+
attrs: {}
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return { element: "span", attrs: {} };
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
// src/content/parser/MarkupParser.ts
|
|
2317
|
+
var MarkupParser = class {
|
|
2318
|
+
constructor(options = {}) {
|
|
2319
|
+
__publicField(this, "styleMapper");
|
|
2320
|
+
this.styleMapper = options.styleMapper ?? new DefaultStyleMapper();
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Parse HTML string into ContentUnit
|
|
2324
|
+
*
|
|
2325
|
+
* Handles two input formats:
|
|
2326
|
+
* 1. HTML tags: `<b>text</b>` → converts to indexed `<1>text</1>` with styles
|
|
2327
|
+
* 2. Already indexed: `<1>text</1>` → passes through as-is (from server)
|
|
2328
|
+
*
|
|
2329
|
+
* @param input - HTML string with inline elements or already indexed
|
|
2330
|
+
* @returns ContentUnit with indexed tags and styles
|
|
2331
|
+
*/
|
|
2332
|
+
parse(input) {
|
|
2333
|
+
const hasIndexedTags = /<\/?[0-9]+>/.test(input);
|
|
2334
|
+
if (hasIndexedTags) {
|
|
2335
|
+
return {
|
|
2336
|
+
text: input,
|
|
2337
|
+
styles: {}
|
|
2338
|
+
// Empty - styles determined by children
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
const styles = {};
|
|
2342
|
+
let tagIndex = 0;
|
|
2343
|
+
const tagStack = [];
|
|
2344
|
+
let result = "";
|
|
2345
|
+
let pos = 0;
|
|
2346
|
+
while (pos < input.length) {
|
|
2347
|
+
const tagMatch = this.matchTag(input, pos);
|
|
2348
|
+
if (!tagMatch) {
|
|
2349
|
+
result += input.slice(pos);
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
result += input.slice(pos, tagMatch.startIndex);
|
|
2353
|
+
if (tagMatch.isClosing) {
|
|
2354
|
+
const matchingOpen = this.findMatchingOpenTag(
|
|
2355
|
+
tagStack,
|
|
2356
|
+
tagMatch.tag.toLowerCase()
|
|
2357
|
+
);
|
|
2358
|
+
if (matchingOpen !== null) {
|
|
2359
|
+
result += `</${matchingOpen}>`;
|
|
2360
|
+
const stackIndex = tagStack.findIndex(
|
|
2361
|
+
(t) => t.index === matchingOpen
|
|
2362
|
+
);
|
|
2363
|
+
if (stackIndex !== -1) {
|
|
2364
|
+
tagStack.splice(stackIndex, 1);
|
|
2365
|
+
}
|
|
2366
|
+
} else {
|
|
2367
|
+
result += input.slice(tagMatch.startIndex, tagMatch.endIndex);
|
|
2368
|
+
}
|
|
2369
|
+
} else if (tagMatch.isSelfClosing) {
|
|
2370
|
+
tagIndex++;
|
|
2371
|
+
const styleList = this.styleMapper.mapElementToStyles(
|
|
2372
|
+
tagMatch.tag,
|
|
2373
|
+
tagMatch.attrs
|
|
2374
|
+
);
|
|
2375
|
+
if (styleList.length > 0) {
|
|
2376
|
+
styles[String(tagIndex)] = styleList;
|
|
2377
|
+
result += `<${tagIndex}/>`;
|
|
2378
|
+
}
|
|
2379
|
+
} else {
|
|
2380
|
+
const styleList = this.styleMapper.mapElementToStyles(
|
|
2381
|
+
tagMatch.tag,
|
|
2382
|
+
tagMatch.attrs
|
|
2383
|
+
);
|
|
2384
|
+
if (styleList.length > 0) {
|
|
2385
|
+
tagIndex++;
|
|
2386
|
+
styles[String(tagIndex)] = styleList;
|
|
2387
|
+
result += `<${tagIndex}>`;
|
|
2388
|
+
tagStack.push({ tag: tagMatch.tag.toLowerCase(), index: tagIndex });
|
|
2389
|
+
} else {
|
|
2390
|
+
const closingIndex = this.findClosingTagIndex(
|
|
2391
|
+
input,
|
|
2392
|
+
tagMatch.endIndex,
|
|
2393
|
+
tagMatch.tag
|
|
2394
|
+
);
|
|
2395
|
+
if (closingIndex !== -1) {
|
|
2396
|
+
const innerContent = input.slice(tagMatch.endIndex, closingIndex);
|
|
2397
|
+
const innerUnit = this.parse(innerContent);
|
|
2398
|
+
const baseIndex = tagIndex;
|
|
2399
|
+
for (const [key, value] of Object.entries(innerUnit.styles)) {
|
|
2400
|
+
const adjustedKey = String(baseIndex + parseInt(key, 10));
|
|
2401
|
+
styles[adjustedKey] = value;
|
|
2402
|
+
}
|
|
2403
|
+
result += innerUnit.text.replace(/<(\/?)\d+>/g, (match, slash) => {
|
|
2404
|
+
const num = parseInt(match.replace(/[</>]/g, ""), 10);
|
|
2405
|
+
return `<${slash}${baseIndex + num}>`;
|
|
2406
|
+
});
|
|
2407
|
+
const closingMatch = this.matchTag(input, closingIndex);
|
|
2408
|
+
pos = closingMatch ? closingMatch.endIndex : closingIndex;
|
|
2409
|
+
const maxInnerIndex = Object.keys(innerUnit.styles).length > 0 ? Math.max(...Object.keys(innerUnit.styles).map(Number)) : 0;
|
|
2410
|
+
tagIndex = baseIndex + maxInnerIndex;
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
pos = tagMatch.endIndex;
|
|
2416
|
+
}
|
|
2417
|
+
while (tagStack.length > 0) {
|
|
2418
|
+
const unclosed = tagStack.pop();
|
|
2419
|
+
result += `</${unclosed.index}>`;
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
text: result,
|
|
2423
|
+
styles
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Match an HTML tag at the given position
|
|
2428
|
+
*/
|
|
2429
|
+
matchTag(input, startPos) {
|
|
2430
|
+
const tagStart = input.indexOf("<", startPos);
|
|
2431
|
+
if (tagStart === -1) return null;
|
|
2432
|
+
const tagPattern = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/;
|
|
2433
|
+
const remaining = input.slice(tagStart);
|
|
2434
|
+
const match = remaining.match(tagPattern);
|
|
2435
|
+
if (!match) {
|
|
2436
|
+
if (tagStart + 1 < input.length) {
|
|
2437
|
+
return this.matchTag(input, tagStart + 1);
|
|
2438
|
+
}
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
|
|
2442
|
+
return {
|
|
2443
|
+
tag: tagName,
|
|
2444
|
+
attrs: this.parseAttributes(attrsString),
|
|
2445
|
+
isClosing: closingSlash === "/",
|
|
2446
|
+
isSelfClosing: selfClosingSlash === "/",
|
|
2447
|
+
startIndex: tagStart,
|
|
2448
|
+
endIndex: tagStart + fullMatch.length
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Parse HTML attributes string into key-value pairs
|
|
2453
|
+
*/
|
|
2454
|
+
parseAttributes(attrsString) {
|
|
2455
|
+
const attrs = {};
|
|
2456
|
+
if (!attrsString.trim()) return attrs;
|
|
2457
|
+
const attrPattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
|
|
2458
|
+
let match;
|
|
2459
|
+
while ((match = attrPattern.exec(attrsString)) !== null) {
|
|
2460
|
+
const [, name, doubleQuoted, singleQuoted, unquoted] = match;
|
|
2461
|
+
const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
|
|
2462
|
+
attrs[name.toLowerCase()] = value;
|
|
2463
|
+
}
|
|
2464
|
+
return attrs;
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Find matching opening tag in the stack
|
|
2468
|
+
*/
|
|
2469
|
+
findMatchingOpenTag(stack, tagName) {
|
|
2470
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
2471
|
+
if (stack[i].tag === tagName) {
|
|
2472
|
+
return stack[i].index;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Find the closing tag index for a given opening tag
|
|
2479
|
+
*/
|
|
2480
|
+
findClosingTagIndex(input, startPos, tagName) {
|
|
2481
|
+
const lowerTag = tagName.toLowerCase();
|
|
2482
|
+
let depth = 1;
|
|
2483
|
+
let pos = startPos;
|
|
2484
|
+
while (pos < input.length && depth > 0) {
|
|
2485
|
+
const tagMatch = this.matchTag(input, pos);
|
|
2486
|
+
if (!tagMatch) break;
|
|
2487
|
+
if (tagMatch.tag.toLowerCase() === lowerTag) {
|
|
2488
|
+
if (tagMatch.isClosing) {
|
|
2489
|
+
depth--;
|
|
2490
|
+
if (depth === 0) {
|
|
2491
|
+
return tagMatch.startIndex;
|
|
2492
|
+
}
|
|
2493
|
+
} else if (!tagMatch.isSelfClosing) {
|
|
2494
|
+
depth++;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
pos = tagMatch.endIndex;
|
|
2498
|
+
}
|
|
2499
|
+
return -1;
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
// src/content/serializer/HTMLSerializer.ts
|
|
2504
|
+
var HTMLSerializer = class {
|
|
2505
|
+
constructor(options = {}) {
|
|
2506
|
+
__publicField(this, "styleMapper");
|
|
2507
|
+
this.styleMapper = options.styleMapper ?? new DefaultStyleMapper();
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Serialize ContentUnit to HTML string
|
|
2511
|
+
*
|
|
2512
|
+
* @param unit - ContentUnit with indexed tags and styles
|
|
2513
|
+
* @returns HTML string with semantic elements
|
|
2514
|
+
*/
|
|
2515
|
+
serialize(unit) {
|
|
2516
|
+
const { text, styles } = unit;
|
|
2517
|
+
const tagPattern = /<(\/?)?(\d+)(\/)?>/g;
|
|
2518
|
+
let result = "";
|
|
2519
|
+
let lastIndex = 0;
|
|
2520
|
+
let match;
|
|
2521
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
2522
|
+
result += text.slice(lastIndex, match.index);
|
|
2523
|
+
const [fullMatch, closingSlash, indexStr, selfClosingSlash] = match;
|
|
2524
|
+
const tagIndex = indexStr;
|
|
2525
|
+
const styleList = styles[tagIndex] || [];
|
|
2526
|
+
if (closingSlash) {
|
|
2527
|
+
const mapping = this.styleMapper.mapStylesToElement(styleList);
|
|
2528
|
+
result += `</${mapping.element}>`;
|
|
2529
|
+
} else if (selfClosingSlash) {
|
|
2530
|
+
const mapping = this.styleMapper.mapStylesToElement(styleList);
|
|
2531
|
+
const attrs = this.formatAttributes(mapping.attrs);
|
|
2532
|
+
result += `<${mapping.element}${attrs} />`;
|
|
2533
|
+
} else {
|
|
2534
|
+
const mapping = this.styleMapper.mapStylesToElement(styleList);
|
|
2535
|
+
const attrs = this.formatAttributes(mapping.attrs);
|
|
2536
|
+
result += `<${mapping.element}${attrs}>`;
|
|
2537
|
+
}
|
|
2538
|
+
lastIndex = match.index + fullMatch.length;
|
|
2539
|
+
}
|
|
2540
|
+
result += text.slice(lastIndex);
|
|
2541
|
+
return result;
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Format attributes object into HTML attribute string
|
|
2545
|
+
*/
|
|
2546
|
+
formatAttributes(attrs) {
|
|
2547
|
+
const entries = Object.entries(attrs);
|
|
2548
|
+
if (entries.length === 0) return "";
|
|
2549
|
+
return " " + entries.map(([key, value]) => `${key}="${this.escapeAttributeValue(value)}"`).join(" ");
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Escape special characters in attribute values
|
|
2553
|
+
*/
|
|
2554
|
+
escapeAttributeValue(value) {
|
|
2555
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
|
|
2559
|
+
// src/cache/CacheManager.ts
|
|
2560
|
+
var CacheManager = class {
|
|
2561
|
+
constructor(handlerChain, bus, getCurrentLocale, loadNamespaces) {
|
|
2562
|
+
__publicField(this, "handlerChain");
|
|
2563
|
+
__publicField(this, "bus");
|
|
2564
|
+
__publicField(this, "currentLocale");
|
|
2565
|
+
__publicField(this, "loadNamespaces");
|
|
2566
|
+
this.handlerChain = handlerChain;
|
|
2567
|
+
this.bus = bus;
|
|
2568
|
+
this.currentLocale = getCurrentLocale;
|
|
2569
|
+
this.loadNamespaces = loadNamespaces;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Get a cached translation entry
|
|
2573
|
+
*
|
|
2574
|
+
* @param key - Full cache key (e.g., 'en:app:welcome')
|
|
2575
|
+
* @returns Cache entry with value and styles, or null if not found
|
|
2576
|
+
*/
|
|
2577
|
+
get(key) {
|
|
2578
|
+
const entries = this.handlerChain.getEntries();
|
|
2579
|
+
for (const handlerName in entries) {
|
|
2580
|
+
const handlerEntries = entries[handlerName];
|
|
2581
|
+
if (handlerEntries && typeof handlerEntries === "object") {
|
|
2582
|
+
if (key in handlerEntries) {
|
|
2583
|
+
const rawValue = handlerEntries[key];
|
|
2584
|
+
try {
|
|
2585
|
+
if (typeof rawValue === "string") {
|
|
2586
|
+
const parsed = JSON.parse(rawValue);
|
|
2587
|
+
if (typeof parsed === "object" && "text" in parsed) {
|
|
2588
|
+
return {
|
|
2589
|
+
value: String(parsed.text),
|
|
2590
|
+
styles: parsed.styles || void 0
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
return { value: rawValue, styles: void 0 };
|
|
2594
|
+
}
|
|
2595
|
+
return {
|
|
2596
|
+
value: JSON.stringify(rawValue),
|
|
2597
|
+
styles: void 0
|
|
2598
|
+
};
|
|
2599
|
+
} catch {
|
|
2600
|
+
return {
|
|
2601
|
+
value: typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue),
|
|
2602
|
+
styles: void 0
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Set a cache entry (for optimistic updates)
|
|
2612
|
+
*
|
|
2613
|
+
* @param key - Full cache key (e.g., 'en:app:welcome')
|
|
2614
|
+
* @param value - Translation value
|
|
2615
|
+
* @param styles - Optional styles for indexed tags
|
|
2616
|
+
*/
|
|
2617
|
+
set(key, value, styles) {
|
|
2618
|
+
const [locale, namespace, ...keyParts] = key.split(":");
|
|
2619
|
+
const translationKey = keyParts.join(":");
|
|
2620
|
+
if (!locale || !namespace || !translationKey) {
|
|
2621
|
+
throw new Error(
|
|
2622
|
+
`Invalid cache key format: "${key}". Expected format: "locale:namespace:key"`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
this.handlerChain.setCache(key, value, styles);
|
|
2626
|
+
this.bus.emit("cache:set", {
|
|
2627
|
+
payload: {
|
|
2628
|
+
key,
|
|
2629
|
+
value,
|
|
2630
|
+
styles,
|
|
2631
|
+
optimistic: true
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Check if a key exists in cache
|
|
2637
|
+
*
|
|
2638
|
+
* @param key - Full cache key (e.g., 'en:app:welcome')
|
|
2639
|
+
* @returns true if key is cached
|
|
2640
|
+
*/
|
|
2641
|
+
has(key) {
|
|
2642
|
+
return this.get(key) !== null;
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Invalidate cache entries and optionally refetch
|
|
2646
|
+
*
|
|
2647
|
+
* Like TanStack Query's invalidateQueries, this clears matching cache entries
|
|
2648
|
+
* and triggers background refetch by default.
|
|
2649
|
+
*
|
|
2650
|
+
* @param options - Filter options for invalidation
|
|
2651
|
+
*
|
|
2652
|
+
* @example
|
|
2653
|
+
* ```typescript
|
|
2654
|
+
* // Invalidate all
|
|
2655
|
+
* await cache.invalidate();
|
|
2656
|
+
*
|
|
2657
|
+
* // Invalidate by namespace
|
|
2658
|
+
* await cache.invalidate({ namespace: 'app' });
|
|
2659
|
+
*
|
|
2660
|
+
* // Invalidate by locale
|
|
2661
|
+
* await cache.invalidate({ locale: 'es' });
|
|
2662
|
+
*
|
|
2663
|
+
* // Invalidate specific key
|
|
2664
|
+
* await cache.invalidate({ key: 'en:app:welcome' });
|
|
2665
|
+
*
|
|
2666
|
+
* // Invalidate with predicate
|
|
2667
|
+
* await cache.invalidate({ predicate: (k) => k.includes('common:') });
|
|
2668
|
+
*
|
|
2669
|
+
* // Invalidate without refetch
|
|
2670
|
+
* await cache.invalidate({ namespace: 'app', refetch: false });
|
|
2671
|
+
* ```
|
|
2672
|
+
*/
|
|
2673
|
+
async invalidate(options = {}) {
|
|
2674
|
+
const { namespace, locale, key, predicate, refetch = true } = options;
|
|
2675
|
+
const currentLocale = this.currentLocale();
|
|
2676
|
+
let keysCleared = 0;
|
|
2677
|
+
if (key) {
|
|
2678
|
+
const [keyLocale, keyNamespace] = key.split(":");
|
|
2679
|
+
if (keyLocale && keyNamespace) {
|
|
2680
|
+
this.handlerChain.clearNamespace(keyLocale, keyNamespace);
|
|
2681
|
+
keysCleared = 1;
|
|
2682
|
+
}
|
|
2683
|
+
} else if (namespace && locale) {
|
|
2684
|
+
this.handlerChain.clearNamespace(locale, namespace);
|
|
2685
|
+
keysCleared = this.countClearedKeys(locale, namespace);
|
|
2686
|
+
} else if (namespace) {
|
|
2687
|
+
this.handlerChain.clearNamespace(currentLocale, namespace);
|
|
2688
|
+
keysCleared = this.countClearedKeys(currentLocale, namespace);
|
|
2689
|
+
} else if (locale) {
|
|
2690
|
+
const entries = this.handlerChain.getEntries();
|
|
2691
|
+
const namespacesToClear = /* @__PURE__ */ new Set();
|
|
2692
|
+
for (const handlerName in entries) {
|
|
2693
|
+
const handlerEntries = entries[handlerName];
|
|
2694
|
+
if (handlerEntries && typeof handlerEntries === "object") {
|
|
2695
|
+
for (const entryKey of Object.keys(handlerEntries)) {
|
|
2696
|
+
if (entryKey.startsWith(`${locale}:`) || entryKey.startsWith(`@${locale}:`)) {
|
|
2697
|
+
const parts = entryKey.replace("@", "").split(":");
|
|
2698
|
+
if (parts[1]) {
|
|
2699
|
+
namespacesToClear.add(parts[1]);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
for (const ns of namespacesToClear) {
|
|
2706
|
+
this.handlerChain.clearNamespace(locale, ns);
|
|
2707
|
+
keysCleared++;
|
|
2708
|
+
}
|
|
2709
|
+
} else if (predicate) {
|
|
2710
|
+
const entries = this.handlerChain.getEntries();
|
|
2711
|
+
const toClear = /* @__PURE__ */ new Map();
|
|
2712
|
+
for (const handlerName in entries) {
|
|
2713
|
+
const handlerEntries = entries[handlerName];
|
|
2714
|
+
if (handlerEntries && typeof handlerEntries === "object") {
|
|
2715
|
+
for (const entryKey of Object.keys(handlerEntries)) {
|
|
2716
|
+
if (predicate(entryKey)) {
|
|
2717
|
+
const parts = entryKey.replace("@", "").split(":");
|
|
2718
|
+
const entryLocale = parts[0];
|
|
2719
|
+
const entryNs = parts[1];
|
|
2720
|
+
if (entryLocale && entryNs) {
|
|
2721
|
+
if (!toClear.has(entryLocale)) {
|
|
2722
|
+
toClear.set(entryLocale, /* @__PURE__ */ new Set());
|
|
2723
|
+
}
|
|
2724
|
+
toClear.get(entryLocale).add(entryNs);
|
|
2725
|
+
keysCleared++;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
for (const [loc, namespaces] of toClear) {
|
|
2732
|
+
for (const ns of namespaces) {
|
|
2733
|
+
this.handlerChain.clearNamespace(loc, ns);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
} else {
|
|
2737
|
+
this.handlerChain.clearCache();
|
|
2738
|
+
keysCleared = -1;
|
|
2739
|
+
}
|
|
2740
|
+
this.bus.emit("cache:invalidate", {
|
|
2741
|
+
payload: {
|
|
2742
|
+
filter: options,
|
|
2743
|
+
keysCleared,
|
|
2744
|
+
refetch
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
if (refetch) {
|
|
2748
|
+
await this.refetchAfterInvalidate(options);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Count keys that would be cleared (for event reporting)
|
|
2753
|
+
*/
|
|
2754
|
+
countClearedKeys(locale, namespace) {
|
|
2755
|
+
const entries = this.handlerChain.getEntries();
|
|
2756
|
+
let count = 0;
|
|
2757
|
+
const prefix = `${locale}:${namespace}:`;
|
|
2758
|
+
const namespaceKey = `@${locale}:${namespace}`;
|
|
2759
|
+
for (const handlerName in entries) {
|
|
2760
|
+
const handlerEntries = entries[handlerName];
|
|
2761
|
+
if (handlerEntries && typeof handlerEntries === "object") {
|
|
2762
|
+
for (const key of Object.keys(handlerEntries)) {
|
|
2763
|
+
if (key.startsWith(prefix) || key === namespaceKey) {
|
|
2764
|
+
count++;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
return count;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Refetch data after invalidation
|
|
2773
|
+
*/
|
|
2774
|
+
async refetchAfterInvalidate(options) {
|
|
2775
|
+
const { namespace, locale, key } = options;
|
|
2776
|
+
const currentLocale = this.currentLocale();
|
|
2777
|
+
if (key) {
|
|
2778
|
+
const [keyLocale, keyNamespace] = key.split(":");
|
|
2779
|
+
if (keyLocale && keyNamespace) {
|
|
2780
|
+
await this.loadNamespaces(keyLocale, [keyNamespace]);
|
|
2781
|
+
}
|
|
2782
|
+
} else if (namespace) {
|
|
2783
|
+
await this.loadNamespaces(locale || currentLocale, [namespace]);
|
|
2784
|
+
} else if (locale) {
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
|
|
2789
|
+
// src/lionRapid.ts
|
|
2790
|
+
var INDEXED_TAG_REGEX = /<\/?[0-9]+>/;
|
|
2791
|
+
var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
|
|
2792
|
+
var LionRapidCore = class {
|
|
2793
|
+
constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
|
|
2794
|
+
__publicField(this, "handlerChain");
|
|
2795
|
+
__publicField(this, "currentLocale");
|
|
2796
|
+
__publicField(this, "fallbackLocale");
|
|
2797
|
+
__publicField(this, "defaultNamespace");
|
|
2798
|
+
__publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
|
|
2799
|
+
__publicField(this, "bus");
|
|
2800
|
+
__publicField(this, "plugins", /* @__PURE__ */ new Map());
|
|
2801
|
+
// 🔥 EMBEDDED: Core integration maps
|
|
2802
|
+
__publicField(this, "formatters", /* @__PURE__ */ new Map());
|
|
2803
|
+
__publicField(this, "processors", /* @__PURE__ */ new Map());
|
|
2804
|
+
// 📡 EVENT-DRIVEN: Just track for cleanup
|
|
2805
|
+
__publicField(this, "eventPlugins", /* @__PURE__ */ new Set());
|
|
2806
|
+
// 📦 CONTENT: Parser and serializer for ContentUnit processing
|
|
2807
|
+
__publicField(this, "markupParser", new MarkupParser());
|
|
2808
|
+
__publicField(this, "htmlSerializer", new HTMLSerializer());
|
|
2809
|
+
// 🔄 CACHE: TanStack Query-inspired cache management
|
|
2810
|
+
__publicField(this, "cache");
|
|
2811
|
+
this.bus = eventEmitter;
|
|
2812
|
+
this.handlerChain = handlerChain;
|
|
2813
|
+
this.currentLocale = defaultLocale;
|
|
2814
|
+
this.fallbackLocale = fallbackLocale;
|
|
2815
|
+
this.defaultNamespace = defaultNamespace;
|
|
2816
|
+
this.cache = new CacheManager(
|
|
2817
|
+
this.handlerChain,
|
|
2818
|
+
this.bus,
|
|
2819
|
+
() => this.currentLocale,
|
|
2820
|
+
(locale, namespaces) => this.loadNamespaces(locale, namespaces)
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* ✅ Register plugin (called by builder)
|
|
2825
|
+
*/
|
|
2826
|
+
registerPlugin(plugin) {
|
|
2827
|
+
this.plugins.set(plugin.name, plugin);
|
|
2828
|
+
if (this.isEmbeddedPlugin(plugin)) {
|
|
2829
|
+
this.integrateEmbeddedPlugin(plugin);
|
|
2830
|
+
}
|
|
2831
|
+
if (this.isEventPlugin(plugin)) {
|
|
2832
|
+
this.eventPlugins.add(plugin);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* 🔥 EMBEDDED: Integrate plugin functionality into core
|
|
2837
|
+
*/
|
|
2838
|
+
integrateEmbeddedPlugin(plugin) {
|
|
2839
|
+
if (plugin.getFormatters) {
|
|
2840
|
+
const pluginFormatters = plugin.getFormatters();
|
|
2841
|
+
Object.entries(pluginFormatters).forEach(([name, formatter]) => {
|
|
2842
|
+
this.formatters.set(name, formatter);
|
|
2843
|
+
logger.debug(`Embedded formatter: ${name} from ${plugin.name}`);
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
if (plugin.getProcessors) {
|
|
2847
|
+
const pluginProcessors = plugin.getProcessors();
|
|
2848
|
+
Object.entries(pluginProcessors).forEach(([name, processor]) => {
|
|
2849
|
+
this.processors.set(name, processor);
|
|
2850
|
+
logger.debug(`Embedded processor: ${name} from ${plugin.name}`);
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* 📦 TRANSLATION API - Returns PLAIN TEXT
|
|
2856
|
+
*
|
|
2857
|
+
* Strips indexed tags and returns safe plain text output.
|
|
2858
|
+
* For HTML output with styles, use tHtml() instead.
|
|
2859
|
+
*
|
|
2860
|
+
* @param key - Translation key
|
|
2861
|
+
* @param params - Optional interpolation parameters
|
|
2862
|
+
* @param defaultValue - Optional default value
|
|
2863
|
+
* @returns Plain text string (safe, no XSS risk)
|
|
2864
|
+
*
|
|
2865
|
+
* @example
|
|
2866
|
+
* ```typescript
|
|
2867
|
+
* t('welcome') // → "Hello World"
|
|
2868
|
+
* t('greeting', { name: 'John' }) // → "Hello John"
|
|
2869
|
+
* t('missing', 'Default text') // → "Default text" (if key missing)
|
|
2870
|
+
* ```
|
|
2871
|
+
*/
|
|
2872
|
+
t(key, ...args) {
|
|
2873
|
+
const [firstArg, secondArg] = args;
|
|
2874
|
+
const request = prepareTranslationRequest(
|
|
2875
|
+
key,
|
|
2876
|
+
this.currentLocale,
|
|
2877
|
+
firstArg,
|
|
2878
|
+
secondArg,
|
|
2879
|
+
this.defaultNamespace,
|
|
2880
|
+
this.fallbackLocale
|
|
2881
|
+
);
|
|
2882
|
+
logger.debug(`Translation requested: ${request.cleanKey}`);
|
|
2883
|
+
const translateBus = this.bus.translation(request);
|
|
2884
|
+
translateBus.request();
|
|
2885
|
+
const syncResult = this.handlerChain.handleSync(request);
|
|
2886
|
+
if (syncResult && syncResult.status === 200) {
|
|
2887
|
+
const result = this.processValue(syncResult.value, request);
|
|
2888
|
+
logger.debug(
|
|
2889
|
+
`Translation found: ${request.cleanKey} from ${syncResult.source} (${syncResult.duration}ms)`
|
|
2890
|
+
);
|
|
2891
|
+
translateBus.success(result);
|
|
2892
|
+
return result;
|
|
2893
|
+
}
|
|
2894
|
+
this.triggerBackgroundLoad(request);
|
|
2895
|
+
const fallbackValue = request.parsedDefaultValue || request.cleanKey;
|
|
2896
|
+
const fallbackResult = this.processValue(fallbackValue, request);
|
|
2897
|
+
logger.debug(`Translation fallback: ${request.cleanKey}`);
|
|
2898
|
+
translateBus.fallback(fallbackResult);
|
|
2899
|
+
return fallbackResult;
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* 📦 HTML TRANSLATION API
|
|
2903
|
+
* Returns translation as HTML string with styles applied.
|
|
2904
|
+
*
|
|
2905
|
+
* Use for innerHTML, dangerouslySetInnerHTML, or server-side rendering.
|
|
2906
|
+
* For React components, prefer using <Trans> instead.
|
|
2907
|
+
*
|
|
2908
|
+
* @param key - Translation key
|
|
2909
|
+
* @param params - Optional interpolation parameters
|
|
2910
|
+
* @param defaultValue - Optional default value (can contain HTML)
|
|
2911
|
+
* @returns HTML string with styles applied
|
|
2912
|
+
*
|
|
2913
|
+
* @example
|
|
2914
|
+
* ```typescript
|
|
2915
|
+
* // Vanilla JS
|
|
2916
|
+
* element.innerHTML = i18n.tHtml('welcome');
|
|
2917
|
+
* // → "Hello <b>World</b>"
|
|
2918
|
+
*
|
|
2919
|
+
* // React (use sparingly)
|
|
2920
|
+
* <span dangerouslySetInnerHTML={{ __html: tHtml('welcome') }} />
|
|
2921
|
+
* ```
|
|
2922
|
+
*/
|
|
2923
|
+
tHtml(key, ...args) {
|
|
2924
|
+
const [firstArg, secondArg] = args;
|
|
2925
|
+
const request = prepareTranslationRequest(
|
|
2926
|
+
key,
|
|
2927
|
+
this.currentLocale,
|
|
2928
|
+
firstArg,
|
|
2929
|
+
secondArg,
|
|
2930
|
+
this.defaultNamespace,
|
|
2931
|
+
this.fallbackLocale
|
|
2932
|
+
);
|
|
2933
|
+
logger.debug(`HTML translation requested: ${request.cleanKey}`);
|
|
2934
|
+
const translateBus = this.bus.translation(request);
|
|
2935
|
+
translateBus.request();
|
|
2936
|
+
const syncResult = this.handlerChain.handleSync(request);
|
|
2937
|
+
if (syncResult && syncResult.status === 200) {
|
|
2938
|
+
const result = this.processValueAsHtml(syncResult.value, request, syncResult.styles);
|
|
2939
|
+
logger.debug(
|
|
2940
|
+
`HTML translation found: ${request.cleanKey} from ${syncResult.source}`
|
|
2941
|
+
);
|
|
2942
|
+
translateBus.success(result);
|
|
2943
|
+
return result;
|
|
2944
|
+
}
|
|
2945
|
+
this.triggerBackgroundLoad(request);
|
|
2946
|
+
const fallbackValue = request.parsedDefaultValue || request.cleanKey;
|
|
2947
|
+
const fallbackResult = this.processValueAsHtml(fallbackValue, request);
|
|
2948
|
+
logger.debug(`HTML translation fallback: ${request.cleanKey}`);
|
|
2949
|
+
translateBus.fallback(fallbackResult);
|
|
2950
|
+
return fallbackResult;
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* 📦 Process value as HTML (for tHtml)
|
|
2954
|
+
*
|
|
2955
|
+
* Converts indexed tags to HTML using styles from result or namespace cache.
|
|
2956
|
+
*
|
|
2957
|
+
* @internal
|
|
2958
|
+
*/
|
|
2959
|
+
processValueAsHtml(value, request, resultStyles) {
|
|
2960
|
+
const processedText = this.processTranslationValue(value, request);
|
|
2961
|
+
if (this.containsIndexedTags(processedText)) {
|
|
2962
|
+
const styles = resultStyles || this.getStylesFromNamespace(request);
|
|
2963
|
+
const unit = {
|
|
2964
|
+
text: processedText,
|
|
2965
|
+
styles
|
|
2966
|
+
};
|
|
2967
|
+
return this.htmlSerializer.serialize(unit);
|
|
2968
|
+
}
|
|
2969
|
+
if (this.containsHtml(processedText)) {
|
|
2970
|
+
const unit = this.markupParser.parse(processedText);
|
|
2971
|
+
return this.htmlSerializer.serialize(unit);
|
|
2972
|
+
}
|
|
2973
|
+
return processedText;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* 📦 Process a value through ContentUnit or ICU pipeline
|
|
2977
|
+
*
|
|
2978
|
+
* Processing priority:
|
|
2979
|
+
* 1. Indexed tags (<1>...</1>) from server ContentUnit → convert to HTML at render time
|
|
2980
|
+
* 2. HTML tags (from defaultValue or legacy) → MarkupParser → ContentUnit → HTML
|
|
2981
|
+
* 3. Plain text → standard ICU processing
|
|
2982
|
+
*
|
|
2983
|
+
* @internal
|
|
2984
|
+
*/
|
|
2985
|
+
processValue(value, request) {
|
|
2986
|
+
if (this.containsIndexedTags(value)) {
|
|
2987
|
+
return this.processIndexedValue(value, request);
|
|
2988
|
+
}
|
|
2989
|
+
if (this.containsHtml(value)) {
|
|
2990
|
+
return this.processHtmlValue(value, request);
|
|
2991
|
+
}
|
|
2992
|
+
return this.processTranslationValue(value, request);
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* 📦 Check if a string contains indexed tags
|
|
2996
|
+
*
|
|
2997
|
+
* Used for auto-detection of server ContentUnit format.
|
|
2998
|
+
*
|
|
2999
|
+
* @param value - String to check
|
|
3000
|
+
* @returns true if indexed tags are detected
|
|
3001
|
+
*/
|
|
3002
|
+
containsIndexedTags(value) {
|
|
3003
|
+
return INDEXED_TAG_REGEX.test(value);
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* 📦 Process indexed value - returns PLAIN TEXT
|
|
3007
|
+
*
|
|
3008
|
+
* Strips indexed tags (<1>text</1>) → "text"
|
|
3009
|
+
* For HTML output, use tHtml() instead.
|
|
3010
|
+
*
|
|
3011
|
+
* @internal
|
|
3012
|
+
*/
|
|
3013
|
+
processIndexedValue(value, request) {
|
|
3014
|
+
const processedText = this.processTranslationValue(value, request);
|
|
3015
|
+
return processedText.replace(/<\/?[0-9]+>/g, "");
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* 📦 Get styles for a translation key from namespace cache (fallback)
|
|
3019
|
+
*
|
|
3020
|
+
* Used when styles are not available in the result (e.g., for fallback values).
|
|
3021
|
+
* Looks up the full ContentUnit from the namespace cache to get styles.
|
|
3022
|
+
*
|
|
3023
|
+
* @internal
|
|
3024
|
+
*/
|
|
3025
|
+
getStylesFromNamespace(request) {
|
|
3026
|
+
if (!request.namespace) {
|
|
3027
|
+
return {};
|
|
3028
|
+
}
|
|
3029
|
+
try {
|
|
3030
|
+
const namespaceCacheKey = createNamespaceCacheKey(
|
|
3031
|
+
request.locale,
|
|
3032
|
+
request.namespace
|
|
3033
|
+
);
|
|
3034
|
+
const namespaceData = this.handlerChain.getNamespaceData(namespaceCacheKey);
|
|
3035
|
+
if (!namespaceData) {
|
|
3036
|
+
return {};
|
|
3037
|
+
}
|
|
3038
|
+
const resolved = resolveNestedKey(namespaceData, request.cleanKey);
|
|
3039
|
+
if (resolved && typeof resolved === "object" && "styles" in resolved) {
|
|
3040
|
+
const contentUnit = resolved;
|
|
3041
|
+
return contentUnit.styles;
|
|
3042
|
+
}
|
|
3043
|
+
return {};
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
logger.debug(`Error getting styles for key ${request.cleanKey}:`, error);
|
|
3046
|
+
return {};
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* 📦 Process HTML value - returns PLAIN TEXT
|
|
3051
|
+
*
|
|
3052
|
+
* Parses HTML to ContentUnit, processes ICU, strips indexed tags.
|
|
3053
|
+
* For HTML output, use tHtml() instead.
|
|
3054
|
+
*
|
|
3055
|
+
* @internal
|
|
3056
|
+
*/
|
|
3057
|
+
processHtmlValue(value, request) {
|
|
3058
|
+
try {
|
|
3059
|
+
const unit = this.markupParser.parse(value);
|
|
3060
|
+
const processedText = this.processTranslationValue(unit.text, request);
|
|
3061
|
+
return processedText.replace(/<\/?[0-9]+>/g, "");
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
logger.warn(`ContentUnit processing failed for "${value}":`, error);
|
|
3064
|
+
return this.processTranslationValue(value, request);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Implementation (handles both overloads)
|
|
3069
|
+
*/
|
|
3070
|
+
processTranslationValue(value, paramsOrRequest, locale, key) {
|
|
3071
|
+
let params;
|
|
3072
|
+
let resolvedLocale;
|
|
3073
|
+
let resolvedKey;
|
|
3074
|
+
if (paramsOrRequest && "cleanKey" in paramsOrRequest) {
|
|
3075
|
+
const request = paramsOrRequest;
|
|
3076
|
+
params = request.parsedParams;
|
|
3077
|
+
resolvedLocale = request.locale;
|
|
3078
|
+
resolvedKey = request.cleanKey;
|
|
3079
|
+
} else {
|
|
3080
|
+
params = paramsOrRequest;
|
|
3081
|
+
resolvedLocale = locale || this.currentLocale;
|
|
3082
|
+
resolvedKey = key;
|
|
3083
|
+
}
|
|
3084
|
+
let processedValue = value;
|
|
3085
|
+
try {
|
|
3086
|
+
processedValue = this.applyStructuralFormatters(
|
|
3087
|
+
processedValue,
|
|
3088
|
+
params,
|
|
3089
|
+
resolvedKey
|
|
3090
|
+
);
|
|
3091
|
+
if (this.formatters.has("interpolate")) {
|
|
3092
|
+
const interpolateFormatter = this.formatters.get("interpolate");
|
|
3093
|
+
processedValue = interpolateFormatter(
|
|
3094
|
+
processedValue,
|
|
3095
|
+
params,
|
|
3096
|
+
resolvedLocale,
|
|
3097
|
+
resolvedKey
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
if (this.processors.size > 0) {
|
|
3101
|
+
for (const [name, processor] of this.processors.entries()) {
|
|
3102
|
+
try {
|
|
3103
|
+
processedValue = processor(processedValue, params, resolvedKey);
|
|
3104
|
+
} catch (error) {
|
|
3105
|
+
logger.warn(`Processor ${name} failed:`, error);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
return processedValue;
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
logger.warn(`Translation processing failed for "${value}":`, error);
|
|
3112
|
+
return value;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* 📦 CONTENT UNIT API
|
|
3117
|
+
* Returns translation as ContentUnit for JSX rendering (Trans component)
|
|
3118
|
+
*
|
|
3119
|
+
* This is the preferred method for components that need to render
|
|
3120
|
+
* styled content with JSX interpolation.
|
|
3121
|
+
*
|
|
3122
|
+
* @param key - Translation key
|
|
3123
|
+
* @param params - Optional interpolation parameters
|
|
3124
|
+
* @param defaultValue - Optional default value (can contain HTML)
|
|
3125
|
+
* @returns ContentUnit with indexed tags and styles
|
|
3126
|
+
*
|
|
3127
|
+
* @example
|
|
3128
|
+
* ```typescript
|
|
3129
|
+
* const unit = i18n.getContentUnit('terms', {}, 'Read our <b>Terms</b>.');
|
|
3130
|
+
* // → { text: "Read our <1>Terms</1>.", styles: {"1": ["bold"]} }
|
|
3131
|
+
* ```
|
|
3132
|
+
*/
|
|
3133
|
+
getContentUnit(key, params, defaultValue) {
|
|
3134
|
+
const request = prepareTranslationRequest(
|
|
3135
|
+
key,
|
|
3136
|
+
this.currentLocale,
|
|
3137
|
+
params,
|
|
3138
|
+
defaultValue,
|
|
3139
|
+
this.defaultNamespace,
|
|
3140
|
+
this.fallbackLocale
|
|
3141
|
+
);
|
|
3142
|
+
const syncResult = this.handlerChain.handleSync(request);
|
|
3143
|
+
const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
|
|
3144
|
+
if (!syncResult || syncResult.status !== 200) {
|
|
3145
|
+
this.triggerBackgroundLoad(request);
|
|
3146
|
+
}
|
|
3147
|
+
return this.markupParser.parse(rawValue);
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* 📦 Process ContentUnit text through ICU formatters
|
|
3151
|
+
*
|
|
3152
|
+
* Used by JSX serializers to process individual text segments.
|
|
3153
|
+
*
|
|
3154
|
+
* @param text - Text segment (may contain ICU expressions like {name})
|
|
3155
|
+
* @param params - Interpolation parameters
|
|
3156
|
+
* @returns Processed text with ICU expressions resolved
|
|
3157
|
+
*/
|
|
3158
|
+
processContentText(text, params) {
|
|
3159
|
+
return this.processTranslationValue(text, params, this.currentLocale);
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* 📦 Check if a string contains HTML tags
|
|
3163
|
+
*
|
|
3164
|
+
* Used for auto-detection of HTML in default values.
|
|
3165
|
+
*
|
|
3166
|
+
* @param value - String to check
|
|
3167
|
+
* @returns true if HTML tags are detected
|
|
3168
|
+
*/
|
|
3169
|
+
containsHtml(value) {
|
|
3170
|
+
return HTML_TAG_REGEX.test(value);
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* ✅ Handle structured formatters (plural, context) - embedded in core
|
|
3174
|
+
*/
|
|
3175
|
+
applyStructuralFormatters(value, params, translationKey) {
|
|
3176
|
+
const translationData = value;
|
|
3177
|
+
const hasCount = (p) => typeof p?.count === "number";
|
|
3178
|
+
const hasContext = (p) => typeof p?.context === "string";
|
|
3179
|
+
if (this.formatters.has("plural") && hasCount(params)) {
|
|
3180
|
+
const pluralFormatter = this.formatters.get("plural");
|
|
3181
|
+
try {
|
|
3182
|
+
const pluralResult = pluralFormatter(
|
|
3183
|
+
translationData,
|
|
3184
|
+
params,
|
|
3185
|
+
this.currentLocale,
|
|
3186
|
+
translationKey
|
|
3187
|
+
);
|
|
3188
|
+
if (pluralResult !== String(translationData)) {
|
|
3189
|
+
return pluralResult;
|
|
3190
|
+
}
|
|
3191
|
+
} catch (error) {
|
|
3192
|
+
logger.warn("Plural formatting failed:", error);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
if (this.formatters.has("context") && hasContext(params)) {
|
|
3196
|
+
const contextFormatter = this.formatters.get("context");
|
|
3197
|
+
try {
|
|
3198
|
+
const contextResult = contextFormatter(
|
|
3199
|
+
translationData,
|
|
3200
|
+
params,
|
|
3201
|
+
this.currentLocale,
|
|
3202
|
+
translationKey
|
|
3203
|
+
);
|
|
3204
|
+
if (contextResult !== String(translationData)) {
|
|
3205
|
+
return contextResult;
|
|
3206
|
+
}
|
|
3207
|
+
} catch (error) {
|
|
3208
|
+
logger.warn("Context formatting failed:", error);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
return String(translationData);
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Trigger background load via handler chain
|
|
3215
|
+
*/
|
|
3216
|
+
triggerBackgroundLoad(request) {
|
|
3217
|
+
logger.debug(`Background load: ${request.cleanKey}`);
|
|
3218
|
+
const cacheKey = request.individualCacheKey;
|
|
3219
|
+
if (!this.pendingTranslations.has(cacheKey)) {
|
|
3220
|
+
const promise = this.handlerChain.handleAsync(request).then((result) => {
|
|
3221
|
+
if (result && result.value) {
|
|
3222
|
+
logger.debug(`Cached: ${request.cleanKey} = "${result.value}"`);
|
|
3223
|
+
} else {
|
|
3224
|
+
logger.debug(`Not found: ${request.cleanKey}`);
|
|
3225
|
+
}
|
|
3226
|
+
}).catch((error) => {
|
|
3227
|
+
logger.warn(`Background load failed: ${request.cleanKey}`, error);
|
|
3228
|
+
});
|
|
3229
|
+
this.pendingTranslations.set(cacheKey, promise);
|
|
3230
|
+
promise.finally(() => this.pendingTranslations.delete(cacheKey));
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Get the current locale
|
|
3235
|
+
*/
|
|
3236
|
+
getCurrentLocale() {
|
|
3237
|
+
return this.currentLocale;
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Set default namespace (useful for scoped translations)
|
|
3241
|
+
*/
|
|
3242
|
+
setDefaultNamespace(namespace) {
|
|
3243
|
+
this.defaultNamespace = namespace;
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Get current default namespace
|
|
3247
|
+
*/
|
|
3248
|
+
getDefaultNamespace() {
|
|
3249
|
+
return this.defaultNamespace;
|
|
3250
|
+
}
|
|
3251
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3252
|
+
getEntries() {
|
|
3253
|
+
return this.handlerChain.getEntries();
|
|
3254
|
+
}
|
|
3255
|
+
isCached(key) {
|
|
3256
|
+
const request = prepareTranslationRequest(
|
|
3257
|
+
key,
|
|
3258
|
+
this.currentLocale,
|
|
3259
|
+
void 0,
|
|
3260
|
+
void 0,
|
|
3261
|
+
this.defaultNamespace,
|
|
3262
|
+
this.fallbackLocale
|
|
3263
|
+
);
|
|
3264
|
+
return this.handlerChain.isCached(request);
|
|
3265
|
+
}
|
|
3266
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3267
|
+
getHandlerChainInfo() {
|
|
3268
|
+
return this.handlerChain.getChainInfo();
|
|
3269
|
+
}
|
|
3270
|
+
/**
|
|
3271
|
+
* ✅ Smart language change with selective cache clearing
|
|
3272
|
+
*/
|
|
3273
|
+
async changeLanguage(newLocale, options) {
|
|
3274
|
+
const oldLocale = this.currentLocale;
|
|
3275
|
+
if (oldLocale === newLocale) {
|
|
3276
|
+
logger.debug(`Language already set to ${newLocale}`);
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const clear = options?.clear || false;
|
|
3280
|
+
const languageBus = this.bus.language(oldLocale, newLocale);
|
|
3281
|
+
try {
|
|
3282
|
+
logger.info(`Language change: ${oldLocale} \u2192 ${newLocale}`);
|
|
3283
|
+
logger.debug("Language change options", {
|
|
3284
|
+
clear,
|
|
3285
|
+
preloadNamespaces: options?.preloadNamespaces
|
|
3286
|
+
});
|
|
3287
|
+
languageBus.start();
|
|
3288
|
+
this.currentLocale = newLocale;
|
|
3289
|
+
if (clear) {
|
|
3290
|
+
await this.clearCache();
|
|
3291
|
+
this.clearOldLocalePendingTranslations(oldLocale);
|
|
3292
|
+
}
|
|
3293
|
+
if (options?.preloadNamespaces?.length) {
|
|
3294
|
+
await this.loadNamespaces(newLocale, options.preloadNamespaces);
|
|
3295
|
+
}
|
|
3296
|
+
languageBus.complete();
|
|
3297
|
+
logger.info(`Language changed: ${newLocale}`);
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
logger.error("Language change failed:", error);
|
|
3300
|
+
languageBus.error(error);
|
|
3301
|
+
throw error;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
async clearCache() {
|
|
3305
|
+
this.handlerChain.clearCache();
|
|
3306
|
+
logger.debug(`Cache cleared for locale: ${this.currentLocale}`);
|
|
3307
|
+
}
|
|
3308
|
+
clearOldLocalePendingTranslations(oldLocale) {
|
|
3309
|
+
let clearedCount = 0;
|
|
3310
|
+
for (const key of this.pendingTranslations.keys()) {
|
|
3311
|
+
if (key.includes(`${oldLocale}:`)) {
|
|
3312
|
+
this.pendingTranslations.delete(key);
|
|
3313
|
+
clearedCount++;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (clearedCount > 0) {
|
|
3317
|
+
logger.debug(
|
|
3318
|
+
`Cleared ${clearedCount} pending translations for ${oldLocale}`
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async loadNamespaces(locale, namespaces) {
|
|
3323
|
+
logger.debug(`Preloading ${namespaces.length} namespaces for ${locale}`);
|
|
3324
|
+
const preloadPromises = namespaces.map(async (namespace) => {
|
|
3325
|
+
try {
|
|
3326
|
+
await this.handlerChain.loadNamespace?.(locale, namespace);
|
|
3327
|
+
logger.debug(`Preloaded: ${locale}:${namespace}`);
|
|
3328
|
+
} catch (error) {
|
|
3329
|
+
logger.warn(`Failed to preload ${locale}:${namespace}:`, error);
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
await Promise.allSettled(preloadPromises);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Reload a namespace by clearing cache and fetching fresh data from server
|
|
3336
|
+
* Useful after syncing new translations to ensure fresh data
|
|
3337
|
+
*
|
|
3338
|
+
* @param namespace - The namespace to reload
|
|
3339
|
+
* @param locale - Optional locale (defaults to current locale)
|
|
3340
|
+
* @deprecated Use cache.invalidate({ namespace, refetch: true }) instead
|
|
3341
|
+
*/
|
|
3342
|
+
async reloadNamespace(namespace, locale) {
|
|
3343
|
+
const targetLocale = locale || this.currentLocale;
|
|
3344
|
+
logger.debug(`Reloading namespace: ${targetLocale}:${namespace}`);
|
|
3345
|
+
await this.cache.invalidate({
|
|
3346
|
+
namespace,
|
|
3347
|
+
locale: targetLocale,
|
|
3348
|
+
refetch: true
|
|
3349
|
+
});
|
|
3350
|
+
logger.info(`Namespace reloaded: ${targetLocale}:${namespace}`);
|
|
3351
|
+
}
|
|
3352
|
+
// Helper methods
|
|
3353
|
+
isEmbeddedPlugin(plugin) {
|
|
3354
|
+
return plugin.capabilities.some(
|
|
3355
|
+
(cap) => ["formatters", "processors", "pluralization", "context"].includes(cap)
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
isEventPlugin(plugin) {
|
|
3359
|
+
return plugin.capabilities.some(
|
|
3360
|
+
(cap) => [
|
|
3361
|
+
"analytics",
|
|
3362
|
+
"logging",
|
|
3363
|
+
"react",
|
|
3364
|
+
"validation",
|
|
3365
|
+
"dev-tools",
|
|
3366
|
+
"monitoring"
|
|
3367
|
+
].includes(cap)
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
|
|
3372
|
+
// src/sync/missing-translation-queue.ts
|
|
3373
|
+
var COOLDOWN_MS = 6e4;
|
|
3374
|
+
var MissingTranslationQueue = class {
|
|
3375
|
+
constructor(repository, config, eventEmitter) {
|
|
3376
|
+
__publicField(this, "queue", /* @__PURE__ */ new Map());
|
|
3377
|
+
__publicField(this, "debounceTimer", null);
|
|
3378
|
+
__publicField(this, "repository");
|
|
3379
|
+
__publicField(this, "config");
|
|
3380
|
+
__publicField(this, "eventEmitter");
|
|
3381
|
+
__publicField(this, "recentlySynced", /* @__PURE__ */ new Map());
|
|
3382
|
+
this.repository = repository;
|
|
3383
|
+
this.config = config;
|
|
3384
|
+
this.eventEmitter = eventEmitter ?? null;
|
|
3385
|
+
if (!config.enabled) {
|
|
3386
|
+
logger.debug("MissingTranslationQueue is disabled");
|
|
3387
|
+
} else {
|
|
3388
|
+
logger.debug(
|
|
3389
|
+
`MissingTranslationQueue: debounce=${config.debounceMs}ms, batchSize=${config.batchSize}`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Add a missing translation to the queue
|
|
3395
|
+
* Triggers debounce timer or immediate flush if batch size reached
|
|
3396
|
+
*/
|
|
3397
|
+
add(item) {
|
|
3398
|
+
if (!this.config.enabled) return;
|
|
3399
|
+
const cacheKey = `${item.locale}:${item.namespace}:${item.key}`;
|
|
3400
|
+
const syncedAt = this.recentlySynced.get(cacheKey);
|
|
3401
|
+
if (syncedAt && Date.now() - syncedAt < COOLDOWN_MS) {
|
|
3402
|
+
logger.debug(`Skipping recently synced key: ${cacheKey}`);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
this.queue.set(cacheKey, item);
|
|
3406
|
+
logger.debug(
|
|
3407
|
+
`Missing translation queued: ${cacheKey} (queue size: ${this.queue.size})`
|
|
3408
|
+
);
|
|
3409
|
+
if (this.debounceTimer) {
|
|
3410
|
+
clearTimeout(this.debounceTimer);
|
|
3411
|
+
this.debounceTimer = null;
|
|
3412
|
+
}
|
|
3413
|
+
if (this.queue.size >= this.config.batchSize) {
|
|
3414
|
+
logger.debug(`Batch size reached (${this.config.batchSize}), flushing`);
|
|
3415
|
+
this.flush();
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
this.debounceTimer = setTimeout(() => {
|
|
3419
|
+
this.flush();
|
|
3420
|
+
}, this.config.debounceMs);
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* Flush the queue - send all queued items to the backend
|
|
3424
|
+
* Fire-and-forget pattern (async but not awaited)
|
|
3425
|
+
*/
|
|
3426
|
+
flush() {
|
|
3427
|
+
if (this.queue.size === 0) return;
|
|
3428
|
+
const now = Date.now();
|
|
3429
|
+
for (const [key, ts] of this.recentlySynced) {
|
|
3430
|
+
if (now - ts >= COOLDOWN_MS) this.recentlySynced.delete(key);
|
|
3431
|
+
}
|
|
3432
|
+
const batch = Array.from(this.queue.entries()).map(([cacheKey, item]) => ({
|
|
3433
|
+
key: cacheKey,
|
|
3434
|
+
value: item.fallbackValue,
|
|
3435
|
+
styles: item.styles
|
|
3436
|
+
}));
|
|
3437
|
+
const batchSize = batch.length;
|
|
3438
|
+
this.queue.clear();
|
|
3439
|
+
if (this.debounceTimer) {
|
|
3440
|
+
clearTimeout(this.debounceTimer);
|
|
3441
|
+
this.debounceTimer = null;
|
|
3442
|
+
}
|
|
3443
|
+
logger.debug(`Flushing ${batchSize} missing translations`);
|
|
3444
|
+
this.syncBatch(batch).catch((error) => {
|
|
3445
|
+
logger.warn(`Failed to sync ${batchSize} missing translations:`, error);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Send batch to backend via repository
|
|
3450
|
+
* Uses repository's retry logic from makeRequest()
|
|
3451
|
+
*/
|
|
3452
|
+
async syncBatch(batch) {
|
|
3453
|
+
try {
|
|
3454
|
+
const hasStyles = batch.some((item) => item.styles && Object.keys(item.styles).length > 0);
|
|
3455
|
+
if (hasStyles && "setBatchContentUnits" in this.repository) {
|
|
3456
|
+
const contentUnitBatch = batch.map((item) => ({
|
|
3457
|
+
key: item.key,
|
|
3458
|
+
value: { text: item.value, styles: item.styles ?? {} }
|
|
3459
|
+
}));
|
|
3460
|
+
await this.repository.setBatchContentUnits(contentUnitBatch);
|
|
3461
|
+
logger.debug(`Synced ${batch.length} missing translations (ContentUnit)`);
|
|
3462
|
+
} else if ("setBatch" in this.repository) {
|
|
3463
|
+
const plainBatch = batch.map((item) => ({ key: item.key, value: item.value }));
|
|
3464
|
+
await this.repository.setBatch(plainBatch);
|
|
3465
|
+
logger.debug(`Synced ${batch.length} missing translations`);
|
|
3466
|
+
} else {
|
|
3467
|
+
logger.warn(
|
|
3468
|
+
"Repository does not support setBatch, syncing individually"
|
|
3469
|
+
);
|
|
3470
|
+
await this.syncIndividually(batch);
|
|
3471
|
+
}
|
|
3472
|
+
const now = Date.now();
|
|
3473
|
+
const syncedKeys = [];
|
|
3474
|
+
for (const item of batch) {
|
|
3475
|
+
this.recentlySynced.set(item.key, now);
|
|
3476
|
+
syncedKeys.push(item.key);
|
|
3477
|
+
}
|
|
3478
|
+
if (this.eventEmitter) {
|
|
3479
|
+
this.eventEmitter.emit("sync:batch:complete", {
|
|
3480
|
+
payload: { count: batch.length, keys: syncedKeys }
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
logger.error("Batch sync failed:", error);
|
|
3485
|
+
throw error;
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Fallback: sync items individually in parallel
|
|
3490
|
+
*/
|
|
3491
|
+
async syncIndividually(batch) {
|
|
3492
|
+
const syncPromises = batch.map(async (item) => {
|
|
3493
|
+
try {
|
|
3494
|
+
await this.repository.set(item.key, item.value);
|
|
3495
|
+
logger.debug(`Synced: ${item.key}`);
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
logger.warn(`Failed to sync ${item.key}:`, error);
|
|
3498
|
+
}
|
|
3499
|
+
});
|
|
3500
|
+
await Promise.allSettled(syncPromises);
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Get current queue size (for debugging)
|
|
3504
|
+
*/
|
|
3505
|
+
getQueueSize() {
|
|
3506
|
+
return this.queue.size;
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Get queue stats (for debugging)
|
|
3510
|
+
*/
|
|
3511
|
+
getStats() {
|
|
3512
|
+
return {
|
|
3513
|
+
queueSize: this.queue.size,
|
|
3514
|
+
enabled: this.config.enabled,
|
|
3515
|
+
debounceMs: this.config.debounceMs,
|
|
3516
|
+
batchSize: this.config.batchSize
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Cleanup - clear timer and queue
|
|
3521
|
+
*/
|
|
3522
|
+
destroy() {
|
|
3523
|
+
if (this.debounceTimer) {
|
|
3524
|
+
clearTimeout(this.debounceTimer);
|
|
3525
|
+
this.debounceTimer = null;
|
|
3526
|
+
}
|
|
3527
|
+
this.queue.clear();
|
|
3528
|
+
this.recentlySynced.clear();
|
|
3529
|
+
logger.debug("MissingTranslationQueue destroyed");
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
|
|
3533
|
+
// src/builder.ts
|
|
3534
|
+
var LionRapidBuilder = class _LionRapidBuilder {
|
|
3535
|
+
constructor(config) {
|
|
3536
|
+
__publicField(this, "config");
|
|
3537
|
+
__publicField(this, "plugins", []);
|
|
3538
|
+
__publicField(this, "syncStorageRepos", []);
|
|
3539
|
+
__publicField(this, "asyncStorageRepos", []);
|
|
3540
|
+
__publicField(this, "networkRepos", []);
|
|
3541
|
+
__publicField(this, "syncQueue", null);
|
|
3542
|
+
this.config = {
|
|
3543
|
+
defaultLocale: config.defaultLocale || "en",
|
|
3544
|
+
fallbackLocale: config.fallbackLocale || "en",
|
|
3545
|
+
namespace: config.namespace || "app"
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
static init(config = {}) {
|
|
3549
|
+
return new _LionRapidBuilder(config);
|
|
3550
|
+
}
|
|
3551
|
+
useSyncStorage(memory) {
|
|
3552
|
+
this.syncStorageRepos.push(memory);
|
|
3553
|
+
return this;
|
|
3554
|
+
}
|
|
3555
|
+
useAsyncStorage(storage) {
|
|
3556
|
+
this.asyncStorageRepos.push(storage);
|
|
3557
|
+
return this;
|
|
3558
|
+
}
|
|
3559
|
+
useNetwork(network) {
|
|
3560
|
+
this.networkRepos.push(network);
|
|
3561
|
+
return this;
|
|
3562
|
+
}
|
|
3563
|
+
use(instance) {
|
|
3564
|
+
switch (instance.type) {
|
|
3565
|
+
case "plugin":
|
|
3566
|
+
this.plugins.push(instance);
|
|
3567
|
+
break;
|
|
3568
|
+
case "memory":
|
|
3569
|
+
this.useSyncStorage(instance);
|
|
3570
|
+
break;
|
|
3571
|
+
case "storage":
|
|
3572
|
+
this.useAsyncStorage(instance);
|
|
3573
|
+
break;
|
|
3574
|
+
case "network":
|
|
3575
|
+
this.useNetwork(instance);
|
|
3576
|
+
break;
|
|
3577
|
+
}
|
|
3578
|
+
return this;
|
|
3579
|
+
}
|
|
3580
|
+
async build() {
|
|
3581
|
+
const eventEmitter = new EventEmitter();
|
|
3582
|
+
const buildBus = eventEmitter.builder();
|
|
3583
|
+
logger.info("Builder started");
|
|
3584
|
+
logger.debug("Builder config", this.config);
|
|
3585
|
+
buildBus.buildStart(this.config.defaultLocale, this.config.namespace);
|
|
3586
|
+
const handlerChain = new HandlerChain(eventEmitter);
|
|
3587
|
+
for (const syncRepo of this.syncStorageRepos) {
|
|
3588
|
+
if (!syncRepo.getConfig().enabled) continue;
|
|
3589
|
+
const syncHandler = new SyncHandler(eventEmitter, syncRepo);
|
|
3590
|
+
buildBus.use("sync");
|
|
3591
|
+
handlerChain.addSyncHandler(syncHandler);
|
|
3592
|
+
}
|
|
3593
|
+
for (const asyncRepo of this.asyncStorageRepos) {
|
|
3594
|
+
if (!asyncRepo.getConfig().enabled) continue;
|
|
3595
|
+
const asyncHandler = new AsyncHandler(eventEmitter, asyncRepo);
|
|
3596
|
+
buildBus.use("async");
|
|
3597
|
+
handlerChain.addAsyncHandler(asyncHandler);
|
|
3598
|
+
}
|
|
3599
|
+
let networkFirst = null;
|
|
3600
|
+
for (const networkRepo of this.networkRepos) {
|
|
3601
|
+
if (!networkRepo.getConfig().enabled || !networkRepo.getConfig().options?.baseUrl)
|
|
3602
|
+
continue;
|
|
3603
|
+
const networkHandler = new NetworkHandler(eventEmitter, networkRepo);
|
|
3604
|
+
if (!networkFirst) {
|
|
3605
|
+
networkFirst = networkHandler;
|
|
3606
|
+
}
|
|
3607
|
+
buildBus.use("network");
|
|
3608
|
+
handlerChain.addAsyncHandler(networkHandler);
|
|
3609
|
+
const config = networkRepo.getConfig();
|
|
3610
|
+
if (config.options?.syncMissing?.enabled) {
|
|
3611
|
+
this.syncQueue = new MissingTranslationQueue(networkRepo, {
|
|
3612
|
+
enabled: config.options.syncMissing.enabled,
|
|
3613
|
+
debounceMs: config.options.syncMissing.debounceMs || 500,
|
|
3614
|
+
batchSize: config.options.syncMissing.batchSize || 50
|
|
3615
|
+
}, eventEmitter);
|
|
3616
|
+
eventEmitter.on("translation:not-found", (envelope) => {
|
|
3617
|
+
if (this.syncQueue) {
|
|
3618
|
+
this.syncQueue.add({
|
|
3619
|
+
key: envelope.payload.key,
|
|
3620
|
+
fallbackValue: envelope.payload.fallbackValue,
|
|
3621
|
+
styles: envelope.payload.styles,
|
|
3622
|
+
locale: envelope.payload.locale,
|
|
3623
|
+
namespace: envelope.payload.namespace
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
});
|
|
3627
|
+
logger.info("Missing translation sync enabled");
|
|
3628
|
+
logger.debug("Sync queue config", config.options.syncMissing);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
logger.debug("Handler chain constructed");
|
|
3632
|
+
const facade = new LionRapidCore(
|
|
3633
|
+
eventEmitter,
|
|
3634
|
+
handlerChain,
|
|
3635
|
+
this.config.defaultLocale,
|
|
3636
|
+
this.config.fallbackLocale,
|
|
3637
|
+
this.config.namespace
|
|
3638
|
+
);
|
|
3639
|
+
for (const plugin of this.plugins) {
|
|
3640
|
+
buildBus.use("plugin");
|
|
3641
|
+
await plugin.initialize({ core: facade, bus: eventEmitter });
|
|
3642
|
+
facade.registerPlugin(plugin);
|
|
3643
|
+
logger.info(`Plugin initialized: ${plugin.name}`);
|
|
3644
|
+
logger.debug(`Plugin version: ${plugin.version}`, plugin.capabilities);
|
|
3645
|
+
}
|
|
3646
|
+
const chainInfo = handlerChain.getChainInfo();
|
|
3647
|
+
if (networkFirst) {
|
|
3648
|
+
await this.executePreloading(networkFirst, handlerChain);
|
|
3649
|
+
}
|
|
3650
|
+
logger.info("Builder completed");
|
|
3651
|
+
buildBus.buildDone(
|
|
3652
|
+
chainInfo.handlers.map((r) => r.name),
|
|
3653
|
+
this.plugins.map((p) => p.name)
|
|
3654
|
+
);
|
|
3655
|
+
return facade;
|
|
3656
|
+
}
|
|
3657
|
+
// ✅ CLEAN: Preloading logic
|
|
3658
|
+
async executePreloading(networkHandler, handlerChain) {
|
|
3659
|
+
const eagerPreloads = networkHandler.getPreload().filter((config) => config.eager !== false);
|
|
3660
|
+
if (!eagerPreloads || eagerPreloads.length === 0) {
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
logger.info(`Preloading ${eagerPreloads.length} namespaces`);
|
|
3664
|
+
logger.debug("Preload targets", {
|
|
3665
|
+
namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
|
|
3666
|
+
});
|
|
3667
|
+
const promises = eagerPreloads.map(async (preloadConfig) => {
|
|
3668
|
+
try {
|
|
3669
|
+
const result = await networkHandler.loadNamespace(
|
|
3670
|
+
preloadConfig.locale,
|
|
3671
|
+
preloadConfig.namespace
|
|
3672
|
+
);
|
|
3673
|
+
if (result && result.data && result.status === 200) {
|
|
3674
|
+
const context = {
|
|
3675
|
+
locale: preloadConfig.locale,
|
|
3676
|
+
namespace: preloadConfig.namespace,
|
|
3677
|
+
fallbackLocale: this.config.fallbackLocale
|
|
3678
|
+
};
|
|
3679
|
+
await handlerChain.saveNamespaceToHandlers(result.data, context);
|
|
3680
|
+
logger.debug(
|
|
3681
|
+
`Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
} catch (error) {
|
|
3685
|
+
logger.warn(
|
|
3686
|
+
`Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
|
|
3687
|
+
error
|
|
3688
|
+
);
|
|
3689
|
+
}
|
|
3690
|
+
});
|
|
3691
|
+
await Promise.allSettled(promises);
|
|
3692
|
+
logger.info("Preloading completed");
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
|
|
3696
|
+
// src/repositories/network-repository.ts
|
|
3697
|
+
var _NetworkRepository = class _NetworkRepository {
|
|
3698
|
+
constructor(config = {}) {
|
|
3699
|
+
__publicField(this, "name", "NetworkRepository");
|
|
3700
|
+
__publicField(this, "type", "network");
|
|
3701
|
+
__publicField(this, "config", {});
|
|
3702
|
+
/** Cached version (opaque string from server) per namespace key "locale:namespace" */
|
|
3703
|
+
__publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
|
|
3704
|
+
/**
|
|
3705
|
+
* Default endpoint templates with placeholder support
|
|
3706
|
+
* Placeholders: :key, :locale, :namespace
|
|
3707
|
+
*/
|
|
3708
|
+
__publicField(this, "DEFAULT_ENDPOINTS", {
|
|
3709
|
+
getTranslation: "/api/integrations/translations/:key",
|
|
3710
|
+
getNamespace: "/api/integrations/translations/:locale/:namespace",
|
|
3711
|
+
setTranslation: "/api/integrations/sync/:key",
|
|
3712
|
+
setNamespace: "/api/integrations/sync/:locale/:namespace",
|
|
3713
|
+
setBatch: "/api/integrations/sync/batch",
|
|
3714
|
+
removeTranslation: "/api/integrations/sync/:key",
|
|
3715
|
+
clearTranslations: "/translations"
|
|
3716
|
+
});
|
|
3717
|
+
this.config = {
|
|
3718
|
+
options: {
|
|
3719
|
+
timeout: config.options?.timeout ?? 5e3,
|
|
3720
|
+
baseUrl: config.options?.baseUrl ?? "",
|
|
3721
|
+
headers: config.options?.headers ?? {}
|
|
3722
|
+
},
|
|
3723
|
+
...config
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Get single translation from API
|
|
3728
|
+
* Uses platform-specific integration endpoint
|
|
3729
|
+
* @throws {TranslationNotFoundError} When translation doesn't exist (404)
|
|
3730
|
+
* @throws {TranslationLockedError} When translation is locked/not ready (423)
|
|
3731
|
+
* @throws {ServerError} When server returns 5xx error
|
|
3732
|
+
* @throws {NetworkError} When network request fails
|
|
3733
|
+
*/
|
|
3734
|
+
async get(key) {
|
|
3735
|
+
if (!this.config?.options?.baseUrl) {
|
|
3736
|
+
logger.debug("API base URL is not set");
|
|
3737
|
+
throw new NetworkError("API base URL is not set");
|
|
3738
|
+
}
|
|
3739
|
+
const url = this.buildUrl("getTranslation", { key });
|
|
3740
|
+
logger.debug(`API GET: ${url}`);
|
|
3741
|
+
try {
|
|
3742
|
+
const response = await this.makeRequest(url, "GET");
|
|
3743
|
+
if (!response.ok) {
|
|
3744
|
+
this.handleHttpError(response, key);
|
|
3745
|
+
}
|
|
3746
|
+
const data = await response.json();
|
|
3747
|
+
logger.debug("API response data:", data);
|
|
3748
|
+
const translation = data.translations[data.meta?.key] || data.translations[key] || Object.values(data.translations)[0];
|
|
3749
|
+
logger.debug(`Fetched translation for key "${key}":`, translation);
|
|
3750
|
+
if (!translation) {
|
|
3751
|
+
throw new TranslationNotFoundError(key);
|
|
3752
|
+
}
|
|
3753
|
+
if (typeof translation === "object" && "text" in translation) {
|
|
3754
|
+
logger.debug(`ContentUnit text: "${translation.text}"`);
|
|
3755
|
+
return translation.text;
|
|
3756
|
+
}
|
|
3757
|
+
return String(translation);
|
|
3758
|
+
} catch (error) {
|
|
3759
|
+
this.handleCaughtError(error);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3763
|
+
* Get single translation as full ContentUnit (with styles)
|
|
3764
|
+
* Used by NetworkHandler for individual key lookups that need styles
|
|
3765
|
+
* @throws {TranslationNotFoundError} When translation doesn't exist (404)
|
|
3766
|
+
* @throws {TranslationLockedError} When translation is locked/not ready (423)
|
|
3767
|
+
* @throws {ServerError} When server returns 5xx error
|
|
3768
|
+
* @throws {NetworkError} When network request fails
|
|
3769
|
+
*/
|
|
3770
|
+
async getContentUnit(key) {
|
|
3771
|
+
if (!this.config?.options?.baseUrl) {
|
|
3772
|
+
logger.debug("API base URL is not set");
|
|
3773
|
+
throw new NetworkError("API base URL is not set");
|
|
3774
|
+
}
|
|
3775
|
+
const url = this.buildUrl("getTranslation", { key });
|
|
3776
|
+
logger.debug(`API GET ContentUnit: ${url}`);
|
|
3777
|
+
try {
|
|
3778
|
+
const response = await this.makeRequest(url, "GET");
|
|
3779
|
+
if (!response.ok) {
|
|
3780
|
+
this.handleHttpError(response, key);
|
|
3781
|
+
}
|
|
3782
|
+
const data = await response.json();
|
|
3783
|
+
logger.debug("API response data:", data);
|
|
3784
|
+
const translation = data.translations[data.meta?.key] || data.translations[key] || Object.values(data.translations)[0];
|
|
3785
|
+
if (!translation) {
|
|
3786
|
+
throw new TranslationNotFoundError(key);
|
|
3787
|
+
}
|
|
3788
|
+
if (typeof translation === "object" && "text" in translation) {
|
|
3789
|
+
logger.debug(`ContentUnit for "${key}": text="${translation.text}", styles=`, translation.styles);
|
|
3790
|
+
return {
|
|
3791
|
+
text: translation.text,
|
|
3792
|
+
styles: translation.styles || {}
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
logger.debug(`Plain string for "${key}": "${translation}" (no styles)`);
|
|
3796
|
+
return {
|
|
3797
|
+
text: String(translation),
|
|
3798
|
+
styles: {}
|
|
3799
|
+
};
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
this.handleCaughtError(error);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* Get namespace with all translations
|
|
3806
|
+
* @throws {TranslationNotFoundError} When namespace doesn't exist (404)
|
|
3807
|
+
* @throws {TranslationLockedError} When namespace is locked/not ready (423)
|
|
3808
|
+
* @throws {ServerError} When server returns 5xx error
|
|
3809
|
+
* @throws {NetworkError} When network request fails
|
|
3810
|
+
*/
|
|
3811
|
+
async getNamespace(locale, namespace) {
|
|
3812
|
+
if (!this.config?.options?.baseUrl) {
|
|
3813
|
+
logger.debug("API base URL is not set");
|
|
3814
|
+
throw new NetworkError("API base URL is not set");
|
|
3815
|
+
}
|
|
3816
|
+
const url = this.buildUrl("getNamespace", { locale, namespace });
|
|
3817
|
+
logger.debug(`API GET Namespace: ${url}`);
|
|
3818
|
+
const namespaceKey = `${locale}:${namespace}`;
|
|
3819
|
+
try {
|
|
3820
|
+
const extraHeaders = {};
|
|
3821
|
+
const cachedVersion = this.getVersion(namespaceKey);
|
|
3822
|
+
if (cachedVersion) {
|
|
3823
|
+
extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
|
|
3824
|
+
}
|
|
3825
|
+
const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
|
|
3826
|
+
if (response.status === 304) {
|
|
3827
|
+
logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
|
|
3828
|
+
return null;
|
|
3829
|
+
}
|
|
3830
|
+
if (!response.ok) {
|
|
3831
|
+
this.handleHttpError(response, namespaceKey);
|
|
3832
|
+
}
|
|
3833
|
+
const data = await response.json();
|
|
3834
|
+
logger.debug("API response data:", data);
|
|
3835
|
+
if (!data.translations) {
|
|
3836
|
+
throw new TranslationNotFoundError(namespaceKey);
|
|
3837
|
+
}
|
|
3838
|
+
const version = data.meta?.version != null ? String(data.meta.version) : null;
|
|
3839
|
+
if (version) {
|
|
3840
|
+
this.setVersion(namespaceKey, version);
|
|
3841
|
+
}
|
|
3842
|
+
return data.translations;
|
|
3843
|
+
} catch (error) {
|
|
3844
|
+
this.handleCaughtError(error);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Save translation to API (if supported)
|
|
3849
|
+
*/
|
|
3850
|
+
async set(key, value) {
|
|
3851
|
+
if (!this.config?.options?.baseUrl) {
|
|
3852
|
+
logger.debug("API base URL is not set");
|
|
3853
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
3854
|
+
}
|
|
3855
|
+
try {
|
|
3856
|
+
const url = this.buildUrl("setTranslation", { key });
|
|
3857
|
+
const response = await this.makeRequest(url, "PUT", {
|
|
3858
|
+
translation: value
|
|
3859
|
+
});
|
|
3860
|
+
if (!response.ok) {
|
|
3861
|
+
throw new Error(`Failed to save translation: ${response.status}`);
|
|
3862
|
+
}
|
|
3863
|
+
} catch (error) {
|
|
3864
|
+
logger.debug(`API set failed for key "${key}":`, error);
|
|
3865
|
+
throw error;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
async setNamespace(locale, namespace, translations) {
|
|
3869
|
+
if (!this.config?.options?.baseUrl) {
|
|
3870
|
+
logger.debug("API base URL is not set");
|
|
3871
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
3872
|
+
}
|
|
3873
|
+
try {
|
|
3874
|
+
const url = this.buildUrl("setNamespace", { locale, namespace });
|
|
3875
|
+
const response = await this.makeRequest(url, "PUT", {
|
|
3876
|
+
translations
|
|
3877
|
+
});
|
|
3878
|
+
if (!response.ok) {
|
|
3879
|
+
throw new Error(`Failed to save namespace: ${response.status}`);
|
|
3880
|
+
}
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
logger.debug(
|
|
3883
|
+
`API setNamespace failed for ${locale}:${namespace}:`,
|
|
3884
|
+
error
|
|
3885
|
+
);
|
|
3886
|
+
throw error;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Batch sync multiple missing translations
|
|
3891
|
+
* Uses new batch endpoint for efficiency
|
|
3892
|
+
*/
|
|
3893
|
+
async setBatch(items) {
|
|
3894
|
+
if (!this.config?.options?.baseUrl) {
|
|
3895
|
+
logger.debug("API base URL is not set");
|
|
3896
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
3897
|
+
}
|
|
3898
|
+
try {
|
|
3899
|
+
const url = this.buildUrl("setBatch");
|
|
3900
|
+
logger.debug(`API POST Batch: ${url} (${items.length} items)`);
|
|
3901
|
+
const response = await this.makeRequest(url, "POST", {
|
|
3902
|
+
translations: items
|
|
3903
|
+
});
|
|
3904
|
+
if (!response.ok) {
|
|
3905
|
+
throw new Error(`Failed to sync batch: ${response.status}`);
|
|
3906
|
+
}
|
|
3907
|
+
logger.debug(`Batch sync successful: ${items.length} items`);
|
|
3908
|
+
} catch (error) {
|
|
3909
|
+
logger.debug(`API setBatch failed for ${items.length} items:`, error);
|
|
3910
|
+
throw error;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
3914
|
+
// ContentUnit API Methods
|
|
3915
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
3916
|
+
/**
|
|
3917
|
+
* Get namespace with all translations as ContentUnit data
|
|
3918
|
+
*
|
|
3919
|
+
* @returns ContentUnitData (Record<string, ContentUnit>)
|
|
3920
|
+
* @throws {TranslationNotFoundError} When namespace doesn't exist (404)
|
|
3921
|
+
* @throws {TranslationLockedError} When namespace is locked/not ready (423)
|
|
3922
|
+
* @throws {ServerError} When server returns 5xx error
|
|
3923
|
+
* @throws {NetworkError} When network request fails
|
|
3924
|
+
*/
|
|
3925
|
+
async getNamespaceContentUnits(locale, namespace) {
|
|
3926
|
+
if (!this.config?.options?.baseUrl) {
|
|
3927
|
+
logger.debug("API base URL is not set");
|
|
3928
|
+
throw new NetworkError("API base URL is not set");
|
|
3929
|
+
}
|
|
3930
|
+
const url = this.buildUrl("getNamespace", { locale, namespace });
|
|
3931
|
+
logger.debug(`API GET Namespace ContentUnits: ${url}`);
|
|
3932
|
+
const namespaceKey = `${locale}:${namespace}`;
|
|
3933
|
+
try {
|
|
3934
|
+
const extraHeaders = {};
|
|
3935
|
+
const cachedVersion = this.getVersion(namespaceKey);
|
|
3936
|
+
if (cachedVersion) {
|
|
3937
|
+
extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
|
|
3938
|
+
}
|
|
3939
|
+
const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
|
|
3940
|
+
if (response.status === 304) {
|
|
3941
|
+
logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
|
|
3942
|
+
return null;
|
|
3943
|
+
}
|
|
3944
|
+
if (!response.ok) {
|
|
3945
|
+
this.handleHttpError(response, namespaceKey);
|
|
3946
|
+
}
|
|
3947
|
+
const data = await response.json();
|
|
3948
|
+
logger.debug("API response data:", data);
|
|
3949
|
+
if (!data.translations) {
|
|
3950
|
+
throw new TranslationNotFoundError(namespaceKey);
|
|
3951
|
+
}
|
|
3952
|
+
const version = data.meta?.version != null ? String(data.meta.version) : null;
|
|
3953
|
+
if (version) {
|
|
3954
|
+
this.setVersion(namespaceKey, version);
|
|
3955
|
+
}
|
|
3956
|
+
return data.translations;
|
|
3957
|
+
} catch (error) {
|
|
3958
|
+
this.handleCaughtError(error);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Save ContentUnit to API
|
|
3963
|
+
*
|
|
3964
|
+
* Sends the ContentUnit format to server for storage.
|
|
3965
|
+
* Server should store in ContentUnit format for efficient retrieval.
|
|
3966
|
+
*
|
|
3967
|
+
* @param key - Translation key
|
|
3968
|
+
* @param unit - ContentUnit to save
|
|
3969
|
+
*/
|
|
3970
|
+
async setContentUnit(key, unit) {
|
|
3971
|
+
if (!this.config?.options?.baseUrl) {
|
|
3972
|
+
logger.debug("API base URL is not set");
|
|
3973
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
3974
|
+
}
|
|
3975
|
+
try {
|
|
3976
|
+
const url = this.buildUrl("setTranslation", { key });
|
|
3977
|
+
const response = await this.makeRequest(url, "PUT", {
|
|
3978
|
+
translation: unit,
|
|
3979
|
+
format: "contentunit"
|
|
3980
|
+
// Signal to server that this is ContentUnit format
|
|
3981
|
+
});
|
|
3982
|
+
if (!response.ok) {
|
|
3983
|
+
throw new Error(`Failed to save ContentUnit: ${response.status}`);
|
|
3984
|
+
}
|
|
3985
|
+
logger.debug(`Saved ContentUnit for key "${key}"`);
|
|
3986
|
+
} catch (error) {
|
|
3987
|
+
logger.debug(`API setContentUnit failed for key "${key}":`, error);
|
|
3988
|
+
throw error;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Batch sync multiple ContentUnits
|
|
3993
|
+
*
|
|
3994
|
+
* Sends ContentUnit format to server for batch storage.
|
|
3995
|
+
*
|
|
3996
|
+
* @param items - Array of key-ContentUnit pairs
|
|
3997
|
+
*/
|
|
3998
|
+
async setBatchContentUnits(items) {
|
|
3999
|
+
if (!this.config?.options?.baseUrl) {
|
|
4000
|
+
logger.debug("API base URL is not set");
|
|
4001
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
4002
|
+
}
|
|
4003
|
+
try {
|
|
4004
|
+
const url = this.buildUrl("setBatch");
|
|
4005
|
+
logger.debug(
|
|
4006
|
+
`API POST Batch ContentUnits: ${url} (${items.length} items)`
|
|
4007
|
+
);
|
|
4008
|
+
const response = await this.makeRequest(url, "POST", {
|
|
4009
|
+
translations: items,
|
|
4010
|
+
format: "contentunit"
|
|
4011
|
+
// Signal to server that this is ContentUnit format
|
|
4012
|
+
});
|
|
4013
|
+
if (!response.ok) {
|
|
4014
|
+
throw new Error(`Failed to sync batch ContentUnits: ${response.status}`);
|
|
4015
|
+
}
|
|
4016
|
+
logger.debug(`Batch ContentUnit sync successful: ${items.length} items`);
|
|
4017
|
+
} catch (error) {
|
|
4018
|
+
logger.debug(
|
|
4019
|
+
`API setBatchContentUnits failed for ${items.length} items:`,
|
|
4020
|
+
error
|
|
4021
|
+
);
|
|
4022
|
+
throw error;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Remove translation from API (if supported)
|
|
4027
|
+
*/
|
|
4028
|
+
async remove(key) {
|
|
4029
|
+
if (!this.config?.options?.baseUrl) {
|
|
4030
|
+
logger.debug("API base URL is not set");
|
|
4031
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
4032
|
+
}
|
|
4033
|
+
try {
|
|
4034
|
+
const url = this.buildUrl("removeTranslation", { key });
|
|
4035
|
+
const response = await this.makeRequest(url, "DELETE");
|
|
4036
|
+
if (!response.ok) {
|
|
4037
|
+
throw new Error(`Failed to remove translation: ${response.status}`);
|
|
4038
|
+
}
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
logger.debug(`API remove failed for key "${key}":`, error);
|
|
4041
|
+
throw error;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Clear all translations (if supported)
|
|
4046
|
+
*/
|
|
4047
|
+
async clear() {
|
|
4048
|
+
if (!this.config?.options?.baseUrl) {
|
|
4049
|
+
logger.debug("API base URL is not set");
|
|
4050
|
+
return Promise.reject(new Error("API base URL is not set"));
|
|
4051
|
+
}
|
|
4052
|
+
try {
|
|
4053
|
+
const url = this.buildUrl("clearTranslations");
|
|
4054
|
+
const response = await this.makeRequest(url, "DELETE");
|
|
4055
|
+
if (!response.ok) {
|
|
4056
|
+
throw new Error(`Failed to clear translations: ${response.status}`);
|
|
4057
|
+
}
|
|
4058
|
+
} catch (error) {
|
|
4059
|
+
logger.debug("API clear failed:", error);
|
|
4060
|
+
throw error;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
getConfig() {
|
|
4064
|
+
return this.config;
|
|
4065
|
+
}
|
|
4066
|
+
/** Read persisted version from localStorage, falling back to in-memory map */
|
|
4067
|
+
getVersion(namespaceKey) {
|
|
4068
|
+
const mem = this.namespaceVersions.get(namespaceKey);
|
|
4069
|
+
if (mem) return mem;
|
|
4070
|
+
try {
|
|
4071
|
+
const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
|
|
4072
|
+
if (stored) {
|
|
4073
|
+
this.namespaceVersions.set(namespaceKey, stored);
|
|
4074
|
+
return stored;
|
|
4075
|
+
}
|
|
4076
|
+
} catch {
|
|
4077
|
+
}
|
|
4078
|
+
return void 0;
|
|
4079
|
+
}
|
|
4080
|
+
/** Persist version to both in-memory map and localStorage */
|
|
4081
|
+
setVersion(namespaceKey, version) {
|
|
4082
|
+
this.namespaceVersions.set(namespaceKey, version);
|
|
4083
|
+
try {
|
|
4084
|
+
localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
|
|
4085
|
+
} catch {
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
/**
|
|
4089
|
+
* Handle HTTP error responses by mapping to domain errors
|
|
4090
|
+
* @throws {TranslationNotFoundError} When status is 404
|
|
4091
|
+
* @throws {TranslationLockedError} When status is 423
|
|
4092
|
+
* @throws {ServerError} When status is 5xx
|
|
4093
|
+
* @throws {NetworkError} For other client errors
|
|
4094
|
+
*/
|
|
4095
|
+
handleHttpError(response, key) {
|
|
4096
|
+
logger.debug(`API response: ${response.status} ${response.statusText}`);
|
|
4097
|
+
if (response.status === 404) {
|
|
4098
|
+
throw new TranslationNotFoundError(key);
|
|
4099
|
+
}
|
|
4100
|
+
if (response.status === 423) {
|
|
4101
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
4102
|
+
throw new TranslationLockedError(
|
|
4103
|
+
key,
|
|
4104
|
+
retryAfter ? parseInt(retryAfter, 10) * 1e3 : void 0
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
if (response.status >= 500 && response.status < 600) {
|
|
4108
|
+
throw new ServerError(key, response.status);
|
|
4109
|
+
}
|
|
4110
|
+
throw new NetworkError(`HTTP ${response.status}: ${response.statusText}`);
|
|
4111
|
+
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Handle caught errors by wrapping non-domain errors
|
|
4114
|
+
* @throws {TranslationError} Always throws - propagates domain errors or wraps unexpected ones
|
|
4115
|
+
*/
|
|
4116
|
+
handleCaughtError(error) {
|
|
4117
|
+
if (error instanceof TranslationNotFoundError || error instanceof TranslationLockedError || error instanceof ServerError || error instanceof NetworkError) {
|
|
4118
|
+
throw error;
|
|
4119
|
+
}
|
|
4120
|
+
throw new NetworkError("Network request failed", error);
|
|
4121
|
+
}
|
|
4122
|
+
/**
|
|
4123
|
+
* Build URL from endpoint template and parameters
|
|
4124
|
+
* Supports placeholders: :key, :locale, :namespace
|
|
4125
|
+
* @param endpoint - Endpoint key from DEFAULT_ENDPOINTS
|
|
4126
|
+
* @param params - Parameters to replace placeholders with
|
|
4127
|
+
* @returns Complete URL with baseUrl + endpoint path
|
|
4128
|
+
*/
|
|
4129
|
+
buildUrl(endpoint, params = {}) {
|
|
4130
|
+
const baseUrl = this.config.options?.baseUrl || "";
|
|
4131
|
+
let endpointTemplate = this.config.options?.endpoints?.[endpoint] || this.DEFAULT_ENDPOINTS[endpoint];
|
|
4132
|
+
if (!endpointTemplate.startsWith("/")) {
|
|
4133
|
+
endpointTemplate = `/${endpointTemplate}`;
|
|
4134
|
+
}
|
|
4135
|
+
let path = endpointTemplate;
|
|
4136
|
+
for (const [key, value] of Object.entries(params)) {
|
|
4137
|
+
const placeholder = new RegExp(`:${key}`, "g");
|
|
4138
|
+
path = path.replace(placeholder, encodeURIComponent(value));
|
|
4139
|
+
}
|
|
4140
|
+
return `${baseUrl}${path}`;
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Make HTTP request with timeout and headers
|
|
4144
|
+
*/
|
|
4145
|
+
async makeRequest(url, method, body, extraHeaders) {
|
|
4146
|
+
const controller = new AbortController();
|
|
4147
|
+
const timeout = this.config.options?.timeout ?? 1e4;
|
|
4148
|
+
const timeoutId = setTimeout(
|
|
4149
|
+
() => controller.abort(),
|
|
4150
|
+
timeout
|
|
4151
|
+
);
|
|
4152
|
+
try {
|
|
4153
|
+
const headers = {
|
|
4154
|
+
"Content-Type": "application/json",
|
|
4155
|
+
...this.config.options?.headers,
|
|
4156
|
+
...extraHeaders
|
|
4157
|
+
};
|
|
4158
|
+
if (this.config.options?.apiKey) {
|
|
4159
|
+
headers["Authorization"] = `Bearer ${this.config.options.apiKey}`;
|
|
4160
|
+
}
|
|
4161
|
+
const response = await fetch(url, {
|
|
4162
|
+
method,
|
|
4163
|
+
headers,
|
|
4164
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
4165
|
+
signal: controller.signal
|
|
4166
|
+
});
|
|
4167
|
+
return response;
|
|
4168
|
+
} finally {
|
|
4169
|
+
clearTimeout(timeoutId);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
};
|
|
4173
|
+
/** localStorage key prefix for persisted versions */
|
|
4174
|
+
__publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
|
|
4175
|
+
var NetworkRepository = _NetworkRepository;
|
|
4176
|
+
|
|
4177
|
+
// src/repositories/memory-repository.ts
|
|
4178
|
+
var MemoryRepository = class {
|
|
4179
|
+
constructor(config) {
|
|
4180
|
+
__publicField(this, "name", "MemoryRepository");
|
|
4181
|
+
__publicField(this, "type", "memory");
|
|
4182
|
+
__publicField(this, "config", {});
|
|
4183
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
4184
|
+
__publicField(this, "accessOrder", /* @__PURE__ */ new Map());
|
|
4185
|
+
__publicField(this, "maxSize", 1e3);
|
|
4186
|
+
this.config = config;
|
|
4187
|
+
this.maxSize = this.config.options?.maxSize || 1e3;
|
|
4188
|
+
}
|
|
4189
|
+
get(key) {
|
|
4190
|
+
const value = this.cache.get(key);
|
|
4191
|
+
if (value !== void 0) {
|
|
4192
|
+
this.accessOrder.set(key, Date.now());
|
|
4193
|
+
return value;
|
|
4194
|
+
}
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
set(key, value) {
|
|
4198
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
4199
|
+
this.evictOldest();
|
|
4200
|
+
}
|
|
4201
|
+
this.cache.set(key, value);
|
|
4202
|
+
this.accessOrder.set(key, Date.now());
|
|
4203
|
+
}
|
|
4204
|
+
remove(key) {
|
|
4205
|
+
this.cache.delete(key);
|
|
4206
|
+
this.accessOrder.delete(key);
|
|
4207
|
+
}
|
|
4208
|
+
clear() {
|
|
4209
|
+
this.cache.clear();
|
|
4210
|
+
this.accessOrder.clear();
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* Clear all entries for a specific namespace
|
|
4214
|
+
* Removes both the namespace cache entry and all individual translation keys
|
|
4215
|
+
*/
|
|
4216
|
+
clearNamespace(locale, namespace) {
|
|
4217
|
+
const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
|
|
4218
|
+
const keyPrefix = `${locale}:${namespace}:`;
|
|
4219
|
+
this.cache.delete(namespaceCacheKey);
|
|
4220
|
+
this.accessOrder.delete(namespaceCacheKey);
|
|
4221
|
+
for (const key of this.cache.keys()) {
|
|
4222
|
+
if (key.startsWith(keyPrefix)) {
|
|
4223
|
+
this.cache.delete(key);
|
|
4224
|
+
this.accessOrder.delete(key);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
getNamespace(locale, namespace) {
|
|
4229
|
+
const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
|
|
4230
|
+
const namespaceData = this.cache.get(namespaceCacheKey);
|
|
4231
|
+
if (!namespaceData) return null;
|
|
4232
|
+
try {
|
|
4233
|
+
return JSON.parse(namespaceData);
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
this.cache.delete(namespaceCacheKey);
|
|
4236
|
+
return null;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
setNamespace(locale, namespace, translations) {
|
|
4240
|
+
const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
|
|
4241
|
+
this.cache.set(namespaceCacheKey, JSON.stringify(translations));
|
|
4242
|
+
}
|
|
4243
|
+
evictOldest() {
|
|
4244
|
+
let oldestKey = "";
|
|
4245
|
+
let oldestTime = Date.now();
|
|
4246
|
+
for (const [key, time] of this.accessOrder.entries()) {
|
|
4247
|
+
if (time < oldestTime) {
|
|
4248
|
+
oldestTime = time;
|
|
4249
|
+
oldestKey = key;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
if (oldestKey) {
|
|
4253
|
+
this.cache.delete(oldestKey);
|
|
4254
|
+
this.accessOrder.delete(oldestKey);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Get all cache entries for debugging
|
|
4259
|
+
*/
|
|
4260
|
+
getEntries() {
|
|
4261
|
+
const entries = {};
|
|
4262
|
+
for (const [key, value] of this.cache.entries()) {
|
|
4263
|
+
entries[key] = value;
|
|
4264
|
+
}
|
|
4265
|
+
return entries;
|
|
4266
|
+
}
|
|
4267
|
+
getConfig() {
|
|
4268
|
+
return this.config;
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
|
|
4272
|
+
// src/content/validator/ContentValidator.ts
|
|
4273
|
+
var ContentValidator = class {
|
|
4274
|
+
/**
|
|
4275
|
+
* Validate a ContentUnit for structural integrity
|
|
4276
|
+
*
|
|
4277
|
+
* @param unit - ContentUnit to validate
|
|
4278
|
+
* @returns Validation result with errors and warnings
|
|
4279
|
+
*/
|
|
4280
|
+
validate(unit) {
|
|
4281
|
+
const errors = [];
|
|
4282
|
+
const warnings = [];
|
|
4283
|
+
this.validateTagBalance(unit.text, errors);
|
|
4284
|
+
this.validateStyleReferences(unit.text, unit.styles, errors, warnings);
|
|
4285
|
+
this.validateICUBraces(unit.text, errors);
|
|
4286
|
+
return {
|
|
4287
|
+
valid: errors.length === 0,
|
|
4288
|
+
errors,
|
|
4289
|
+
warnings
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Validate that all indexed tags are properly balanced and correctly nested
|
|
4294
|
+
*/
|
|
4295
|
+
validateTagBalance(text, errors) {
|
|
4296
|
+
const tagPattern = /<(\/?)?(\d+)(\/)?>/g;
|
|
4297
|
+
const tagStack = [];
|
|
4298
|
+
let match;
|
|
4299
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
4300
|
+
const [, closingSlash, indexStr, selfClosingSlash] = match;
|
|
4301
|
+
if (selfClosingSlash) {
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
4304
|
+
if (closingSlash) {
|
|
4305
|
+
if (tagStack.length === 0) {
|
|
4306
|
+
errors.push({
|
|
4307
|
+
code: "UNBALANCED_TAGS",
|
|
4308
|
+
message: `Closing tag </${indexStr}> has no matching opening tag`,
|
|
4309
|
+
path: `text[${match.index}]`
|
|
4310
|
+
});
|
|
4311
|
+
} else {
|
|
4312
|
+
const lastOpen = tagStack[tagStack.length - 1];
|
|
4313
|
+
if (lastOpen.index === indexStr) {
|
|
4314
|
+
tagStack.pop();
|
|
4315
|
+
} else {
|
|
4316
|
+
const foundIndex = tagStack.findIndex((t) => t.index === indexStr);
|
|
4317
|
+
if (foundIndex >= 0) {
|
|
4318
|
+
errors.push({
|
|
4319
|
+
code: "UNBALANCED_TAGS",
|
|
4320
|
+
message: `Improperly nested: </${indexStr}> should close before </${lastOpen.index}>`,
|
|
4321
|
+
path: `text[${match.index}]`
|
|
4322
|
+
});
|
|
4323
|
+
tagStack.splice(foundIndex, 1);
|
|
4324
|
+
} else {
|
|
4325
|
+
errors.push({
|
|
4326
|
+
code: "UNBALANCED_TAGS",
|
|
4327
|
+
message: `Closing tag </${indexStr}> has no matching opening tag`,
|
|
4328
|
+
path: `text[${match.index}]`
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
} else {
|
|
4334
|
+
tagStack.push({ index: indexStr, position: match.index });
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
for (const { index } of tagStack) {
|
|
4338
|
+
errors.push({
|
|
4339
|
+
code: "UNBALANCED_TAGS",
|
|
4340
|
+
message: `Opening tag <${index}> has no matching closing tag`,
|
|
4341
|
+
path: "text"
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
/**
|
|
4346
|
+
* Validate style references - check that used tags have styles and vice versa
|
|
4347
|
+
*/
|
|
4348
|
+
validateStyleReferences(text, styles, errors, warnings) {
|
|
4349
|
+
const usedIndices = /* @__PURE__ */ new Set();
|
|
4350
|
+
const tagPattern = /<\/?(\d+)(?:\/)?>/g;
|
|
4351
|
+
let match;
|
|
4352
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
4353
|
+
usedIndices.add(match[1]);
|
|
4354
|
+
}
|
|
4355
|
+
for (const index of usedIndices) {
|
|
4356
|
+
if (!styles[index] || styles[index].length === 0) {
|
|
4357
|
+
warnings.push({
|
|
4358
|
+
code: "EMPTY_STYLE",
|
|
4359
|
+
message: `Tag <${index}> has no style definition, will render as <span>`
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
for (const index of Object.keys(styles)) {
|
|
4364
|
+
if (!usedIndices.has(index)) {
|
|
4365
|
+
warnings.push({
|
|
4366
|
+
code: "UNUSED_TAG",
|
|
4367
|
+
message: `Style for tag <${index}> is defined but tag is not used in text`
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Validate ICU MessageFormat brace balance
|
|
4374
|
+
*/
|
|
4375
|
+
validateICUBraces(text, errors) {
|
|
4376
|
+
let depth = 0;
|
|
4377
|
+
let inString = false;
|
|
4378
|
+
let stringChar = "";
|
|
4379
|
+
for (let i = 0; i < text.length; i++) {
|
|
4380
|
+
const char = text[i];
|
|
4381
|
+
const prevChar = i > 0 ? text[i - 1] : "";
|
|
4382
|
+
if ((char === '"' || char === "'") && prevChar !== "\\") {
|
|
4383
|
+
if (!inString) {
|
|
4384
|
+
inString = true;
|
|
4385
|
+
stringChar = char;
|
|
4386
|
+
} else if (char === stringChar) {
|
|
4387
|
+
inString = false;
|
|
4388
|
+
stringChar = "";
|
|
4389
|
+
}
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
if (inString) continue;
|
|
4393
|
+
if (char === "{") {
|
|
4394
|
+
depth++;
|
|
4395
|
+
} else if (char === "}") {
|
|
4396
|
+
depth--;
|
|
4397
|
+
if (depth < 0) {
|
|
4398
|
+
errors.push({
|
|
4399
|
+
code: "INVALID_ICU",
|
|
4400
|
+
message: `Unbalanced ICU braces: unexpected '}' at position ${i}`,
|
|
4401
|
+
path: `text[${i}]`
|
|
4402
|
+
});
|
|
4403
|
+
depth = 0;
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
if (depth > 0) {
|
|
4408
|
+
errors.push({
|
|
4409
|
+
code: "INVALID_ICU",
|
|
4410
|
+
message: `Unbalanced ICU braces: ${depth} unclosed '{'`,
|
|
4411
|
+
path: "text"
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
const emptyVarPattern = /\{\s*[,}]/;
|
|
4415
|
+
if (emptyVarPattern.test(text)) {
|
|
4416
|
+
errors.push({
|
|
4417
|
+
code: "INVALID_ICU",
|
|
4418
|
+
message: "ICU variable name is empty or missing",
|
|
4419
|
+
path: "text"
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
};
|
|
4424
|
+
|
|
4425
|
+
// src/browser/storage/localstorage-repository.ts
|
|
4426
|
+
var STORAGE_PREFIX = "lionrapid:";
|
|
4427
|
+
var VERSION_KEY = `${STORAGE_PREFIX}version`;
|
|
4428
|
+
var CURRENT_VERSION = "1";
|
|
4429
|
+
var LocalStorageRepository = class {
|
|
4430
|
+
constructor(config = {}) {
|
|
4431
|
+
__publicField(this, "name", "LocalStorageRepository");
|
|
4432
|
+
__publicField(this, "type", "memory");
|
|
4433
|
+
__publicField(this, "config");
|
|
4434
|
+
__publicField(this, "available");
|
|
4435
|
+
__publicField(this, "prefix");
|
|
4436
|
+
__publicField(this, "defaultTTL");
|
|
4437
|
+
__publicField(this, "maxSize");
|
|
4438
|
+
this.config = {
|
|
4439
|
+
enabled: config.enabled ?? true,
|
|
4440
|
+
priority: config.priority ?? 0,
|
|
4441
|
+
options: {
|
|
4442
|
+
prefix: config.options?.prefix ?? STORAGE_PREFIX,
|
|
4443
|
+
ttl: config.options?.ttl ?? 24 * 60 * 60 * 1e3,
|
|
4444
|
+
maxSize: config.options?.maxSize ?? 500,
|
|
4445
|
+
...config.options
|
|
4446
|
+
}
|
|
4447
|
+
};
|
|
4448
|
+
this.prefix = this.config.options?.prefix ?? STORAGE_PREFIX;
|
|
4449
|
+
this.defaultTTL = this.config.options?.ttl ?? 24 * 60 * 60 * 1e3;
|
|
4450
|
+
this.maxSize = this.config.options?.maxSize ?? 500;
|
|
4451
|
+
this.available = this.checkAvailability();
|
|
4452
|
+
if (this.available) {
|
|
4453
|
+
this.migrateIfNeeded();
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
checkAvailability() {
|
|
4457
|
+
if (typeof window === "undefined") {
|
|
4458
|
+
return false;
|
|
4459
|
+
}
|
|
4460
|
+
try {
|
|
4461
|
+
const testKey = `${this.prefix}__test__`;
|
|
4462
|
+
window.localStorage.setItem(testKey, "test");
|
|
4463
|
+
window.localStorage.removeItem(testKey);
|
|
4464
|
+
return true;
|
|
4465
|
+
} catch {
|
|
4466
|
+
return false;
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
migrateIfNeeded() {
|
|
4470
|
+
try {
|
|
4471
|
+
const storedVersion = window.localStorage.getItem(VERSION_KEY);
|
|
4472
|
+
if (storedVersion !== CURRENT_VERSION) {
|
|
4473
|
+
this.clear();
|
|
4474
|
+
window.localStorage.setItem(VERSION_KEY, CURRENT_VERSION);
|
|
4475
|
+
}
|
|
4476
|
+
} catch {
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
get(key) {
|
|
4480
|
+
if (!this.available) return null;
|
|
4481
|
+
try {
|
|
4482
|
+
const fullKey = this.prefix + key;
|
|
4483
|
+
const itemStr = window.localStorage.getItem(fullKey);
|
|
4484
|
+
if (!itemStr) return null;
|
|
4485
|
+
const item = JSON.parse(itemStr);
|
|
4486
|
+
if (item.expiry !== null && Date.now() > item.expiry) {
|
|
4487
|
+
window.localStorage.removeItem(fullKey);
|
|
4488
|
+
return null;
|
|
4489
|
+
}
|
|
4490
|
+
return item.value;
|
|
4491
|
+
} catch {
|
|
4492
|
+
return null;
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
set(key, value) {
|
|
4496
|
+
if (!this.available) return;
|
|
4497
|
+
const fullKey = this.prefix + key;
|
|
4498
|
+
const item = {
|
|
4499
|
+
value,
|
|
4500
|
+
expiry: this.defaultTTL ? Date.now() + this.defaultTTL : null,
|
|
4501
|
+
timestamp: Date.now()
|
|
4502
|
+
};
|
|
4503
|
+
try {
|
|
4504
|
+
const entryCount = this.getEntryCount();
|
|
4505
|
+
if (entryCount >= this.maxSize) {
|
|
4506
|
+
this.evictOldest();
|
|
4507
|
+
}
|
|
4508
|
+
window.localStorage.setItem(fullKey, JSON.stringify(item));
|
|
4509
|
+
} catch (error) {
|
|
4510
|
+
if (this.isQuotaError(error)) {
|
|
4511
|
+
this.evictOldest();
|
|
4512
|
+
try {
|
|
4513
|
+
window.localStorage.setItem(fullKey, JSON.stringify(item));
|
|
4514
|
+
} catch {
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
remove(key) {
|
|
4520
|
+
if (!this.available) return;
|
|
4521
|
+
try {
|
|
4522
|
+
window.localStorage.removeItem(this.prefix + key);
|
|
4523
|
+
} catch {
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
clear() {
|
|
4527
|
+
if (!this.available) return;
|
|
4528
|
+
try {
|
|
4529
|
+
const keysToRemove = [];
|
|
4530
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
4531
|
+
const key = window.localStorage.key(i);
|
|
4532
|
+
if (key?.startsWith(this.prefix) && key !== VERSION_KEY) {
|
|
4533
|
+
keysToRemove.push(key);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
keysToRemove.forEach((k) => window.localStorage.removeItem(k));
|
|
4537
|
+
} catch {
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
clearNamespace(locale, namespace) {
|
|
4541
|
+
if (!this.available) return;
|
|
4542
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4543
|
+
const keyPrefix = `${locale}:${namespace}:`;
|
|
4544
|
+
try {
|
|
4545
|
+
const keysToRemove = [];
|
|
4546
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
4547
|
+
const key = window.localStorage.key(i);
|
|
4548
|
+
if (!key?.startsWith(this.prefix)) continue;
|
|
4549
|
+
const innerKey = key.slice(this.prefix.length);
|
|
4550
|
+
if (innerKey === namespaceCacheKey || innerKey.startsWith(keyPrefix)) {
|
|
4551
|
+
keysToRemove.push(key);
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
keysToRemove.forEach((k) => window.localStorage.removeItem(k));
|
|
4555
|
+
} catch {
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
getNamespace(locale, namespace) {
|
|
4559
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4560
|
+
const value = this.get(namespaceCacheKey);
|
|
4561
|
+
if (!value) return null;
|
|
4562
|
+
try {
|
|
4563
|
+
return JSON.parse(value);
|
|
4564
|
+
} catch {
|
|
4565
|
+
this.remove(namespaceCacheKey);
|
|
4566
|
+
return null;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
setNamespace(locale, namespace, translations) {
|
|
4570
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4571
|
+
this.set(namespaceCacheKey, JSON.stringify(translations));
|
|
4572
|
+
}
|
|
4573
|
+
getEntries() {
|
|
4574
|
+
const entries = {};
|
|
4575
|
+
if (!this.available) return entries;
|
|
4576
|
+
try {
|
|
4577
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
4578
|
+
const fullKey = window.localStorage.key(i);
|
|
4579
|
+
if (!fullKey?.startsWith(this.prefix) || fullKey === VERSION_KEY) {
|
|
4580
|
+
continue;
|
|
4581
|
+
}
|
|
4582
|
+
const key = fullKey.slice(this.prefix.length);
|
|
4583
|
+
const value = this.get(key);
|
|
4584
|
+
if (value !== null) {
|
|
4585
|
+
entries[key] = value;
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
} catch {
|
|
4589
|
+
}
|
|
4590
|
+
return entries;
|
|
4591
|
+
}
|
|
4592
|
+
getConfig() {
|
|
4593
|
+
return this.config;
|
|
4594
|
+
}
|
|
4595
|
+
isAvailable() {
|
|
4596
|
+
return this.available;
|
|
4597
|
+
}
|
|
4598
|
+
getEntryCount() {
|
|
4599
|
+
if (!this.available) return 0;
|
|
4600
|
+
let count = 0;
|
|
4601
|
+
try {
|
|
4602
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
4603
|
+
const key = window.localStorage.key(i);
|
|
4604
|
+
if (key?.startsWith(this.prefix) && key !== VERSION_KEY) {
|
|
4605
|
+
count++;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
} catch {
|
|
4609
|
+
}
|
|
4610
|
+
return count;
|
|
4611
|
+
}
|
|
4612
|
+
isQuotaError(error) {
|
|
4613
|
+
if (error instanceof DOMException) {
|
|
4614
|
+
return error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED";
|
|
4615
|
+
}
|
|
4616
|
+
return false;
|
|
4617
|
+
}
|
|
4618
|
+
evictOldest() {
|
|
4619
|
+
if (!this.available) return;
|
|
4620
|
+
try {
|
|
4621
|
+
const entries = [];
|
|
4622
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
4623
|
+
const fullKey = window.localStorage.key(i);
|
|
4624
|
+
if (!fullKey?.startsWith(this.prefix) || fullKey === VERSION_KEY) {
|
|
4625
|
+
continue;
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
const itemStr = window.localStorage.getItem(fullKey);
|
|
4629
|
+
if (itemStr) {
|
|
4630
|
+
const item = JSON.parse(itemStr);
|
|
4631
|
+
entries.push({ key: fullKey, timestamp: item.timestamp });
|
|
4632
|
+
}
|
|
4633
|
+
} catch {
|
|
4634
|
+
window.localStorage.removeItem(fullKey);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
4638
|
+
const removeCount = Math.max(1, Math.floor(entries.length * 0.25));
|
|
4639
|
+
for (let i = 0; i < removeCount && i < entries.length; i++) {
|
|
4640
|
+
window.localStorage.removeItem(entries[i].key);
|
|
4641
|
+
}
|
|
4642
|
+
} catch {
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
getCacheStats() {
|
|
4646
|
+
const size = this.getEntryCount();
|
|
4647
|
+
const usage = this.maxSize > 0 ? `${Math.round(size / this.maxSize * 100)}%` : "0%";
|
|
4648
|
+
return {
|
|
4649
|
+
size,
|
|
4650
|
+
maxSize: this.maxSize,
|
|
4651
|
+
usage
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
|
|
4656
|
+
// src/browser/storage/cache-storage-repository.ts
|
|
4657
|
+
var CACHE_NAME = "lionrapid-v1";
|
|
4658
|
+
var CACHE_PREFIX = "lionrapid:";
|
|
4659
|
+
var CacheStorageRepository = class {
|
|
4660
|
+
constructor(config = {}) {
|
|
4661
|
+
__publicField(this, "name", "CacheStorageRepository");
|
|
4662
|
+
__publicField(this, "type", "storage");
|
|
4663
|
+
__publicField(this, "config");
|
|
4664
|
+
__publicField(this, "cacheName");
|
|
4665
|
+
__publicField(this, "defaultTTL");
|
|
4666
|
+
__publicField(this, "available");
|
|
4667
|
+
this.config = {
|
|
4668
|
+
enabled: config.enabled ?? true,
|
|
4669
|
+
priority: config.priority ?? 0,
|
|
4670
|
+
options: {
|
|
4671
|
+
cacheName: config.options?.cacheName ?? CACHE_NAME,
|
|
4672
|
+
ttl: config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3,
|
|
4673
|
+
...config.options
|
|
4674
|
+
}
|
|
4675
|
+
};
|
|
4676
|
+
this.cacheName = this.config.options?.cacheName ?? CACHE_NAME;
|
|
4677
|
+
this.defaultTTL = this.config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
|
|
4678
|
+
this.available = this.checkAvailability();
|
|
4679
|
+
}
|
|
4680
|
+
checkAvailability() {
|
|
4681
|
+
return typeof caches !== "undefined";
|
|
4682
|
+
}
|
|
4683
|
+
async get(key) {
|
|
4684
|
+
if (!this.available) {
|
|
4685
|
+
throw new TranslationNotFoundError(key);
|
|
4686
|
+
}
|
|
4687
|
+
try {
|
|
4688
|
+
const cache = await caches.open(this.cacheName);
|
|
4689
|
+
const response = await cache.match(CACHE_PREFIX + key);
|
|
4690
|
+
if (!response) {
|
|
4691
|
+
throw new TranslationNotFoundError(key);
|
|
4692
|
+
}
|
|
4693
|
+
const data = await response.json();
|
|
4694
|
+
if (data.expiry !== null && Date.now() > data.expiry) {
|
|
4695
|
+
await cache.delete(CACHE_PREFIX + key);
|
|
4696
|
+
throw new TranslationNotFoundError(key);
|
|
4697
|
+
}
|
|
4698
|
+
return data.value;
|
|
4699
|
+
} catch (error) {
|
|
4700
|
+
if (error instanceof TranslationNotFoundError) {
|
|
4701
|
+
throw error;
|
|
4702
|
+
}
|
|
4703
|
+
throw new TranslationNotFoundError(key);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
async set(key, value) {
|
|
4707
|
+
if (!this.available) return;
|
|
4708
|
+
try {
|
|
4709
|
+
const cache = await caches.open(this.cacheName);
|
|
4710
|
+
const data = {
|
|
4711
|
+
value,
|
|
4712
|
+
expiry: this.defaultTTL ? Date.now() + this.defaultTTL : null,
|
|
4713
|
+
timestamp: Date.now()
|
|
4714
|
+
};
|
|
4715
|
+
await cache.put(
|
|
4716
|
+
CACHE_PREFIX + key,
|
|
4717
|
+
new Response(JSON.stringify(data), {
|
|
4718
|
+
headers: { "Content-Type": "application/json" }
|
|
4719
|
+
})
|
|
4720
|
+
);
|
|
4721
|
+
} catch {
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
async remove(key) {
|
|
4725
|
+
if (!this.available) return;
|
|
4726
|
+
try {
|
|
4727
|
+
const cache = await caches.open(this.cacheName);
|
|
4728
|
+
await cache.delete(CACHE_PREFIX + key);
|
|
4729
|
+
} catch {
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
async clear() {
|
|
4733
|
+
if (!this.available) return;
|
|
4734
|
+
try {
|
|
4735
|
+
const cache = await caches.open(this.cacheName);
|
|
4736
|
+
const keys = await cache.keys();
|
|
4737
|
+
await Promise.all(
|
|
4738
|
+
keys.filter((request) => request.url.includes(CACHE_PREFIX)).map((request) => cache.delete(request))
|
|
4739
|
+
);
|
|
4740
|
+
} catch {
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
async clearNamespace(locale, namespace) {
|
|
4744
|
+
if (!this.available) return;
|
|
4745
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4746
|
+
const keyPrefix = `${locale}:${namespace}:`;
|
|
4747
|
+
try {
|
|
4748
|
+
const cache = await caches.open(this.cacheName);
|
|
4749
|
+
const keys = await cache.keys();
|
|
4750
|
+
await Promise.all(
|
|
4751
|
+
keys.filter((request) => {
|
|
4752
|
+
const url = request.url;
|
|
4753
|
+
const key = url.includes(CACHE_PREFIX) ? url.substring(url.indexOf(CACHE_PREFIX) + CACHE_PREFIX.length) : "";
|
|
4754
|
+
return key === namespaceCacheKey || key.startsWith(keyPrefix);
|
|
4755
|
+
}).map((request) => cache.delete(request))
|
|
4756
|
+
);
|
|
4757
|
+
} catch {
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
async getNamespace(locale, namespace) {
|
|
4761
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4762
|
+
const value = await this.get(namespaceCacheKey);
|
|
4763
|
+
try {
|
|
4764
|
+
return JSON.parse(value);
|
|
4765
|
+
} catch {
|
|
4766
|
+
await this.remove(namespaceCacheKey);
|
|
4767
|
+
throw new TranslationNotFoundError(namespaceCacheKey);
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
async setNamespace(locale, namespace, translations) {
|
|
4771
|
+
const namespaceCacheKey = `@${locale}:${namespace}`;
|
|
4772
|
+
await this.set(namespaceCacheKey, JSON.stringify(translations));
|
|
4773
|
+
}
|
|
4774
|
+
getConfig() {
|
|
4775
|
+
return this.config;
|
|
4776
|
+
}
|
|
4777
|
+
isAvailable() {
|
|
4778
|
+
return this.available;
|
|
4779
|
+
}
|
|
4780
|
+
async getEntries() {
|
|
4781
|
+
const entries = {};
|
|
4782
|
+
if (!this.available) return entries;
|
|
4783
|
+
try {
|
|
4784
|
+
const cache = await caches.open(this.cacheName);
|
|
4785
|
+
const keys = await cache.keys();
|
|
4786
|
+
for (const request of keys) {
|
|
4787
|
+
const url = request.url;
|
|
4788
|
+
if (!url.includes(CACHE_PREFIX)) continue;
|
|
4789
|
+
const key = url.substring(
|
|
4790
|
+
url.indexOf(CACHE_PREFIX) + CACHE_PREFIX.length
|
|
4791
|
+
);
|
|
4792
|
+
try {
|
|
4793
|
+
const value = await this.get(key);
|
|
4794
|
+
entries[key] = value;
|
|
4795
|
+
} catch {
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
} catch {
|
|
4799
|
+
}
|
|
4800
|
+
return entries;
|
|
4801
|
+
}
|
|
4802
|
+
async getCacheStats() {
|
|
4803
|
+
if (!this.available) {
|
|
4804
|
+
return { size: 0, cacheName: this.cacheName };
|
|
4805
|
+
}
|
|
4806
|
+
try {
|
|
4807
|
+
const cache = await caches.open(this.cacheName);
|
|
4808
|
+
const keys = await cache.keys();
|
|
4809
|
+
const size = keys.filter((r) => r.url.includes(CACHE_PREFIX)).length;
|
|
4810
|
+
return { size, cacheName: this.cacheName };
|
|
4811
|
+
} catch {
|
|
4812
|
+
return { size: 0, cacheName: this.cacheName };
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
};
|
|
4816
|
+
|
|
4817
|
+
// src/index.ts
|
|
4818
|
+
var VERSION = "1.0.0";
|
|
4819
|
+
//# sourceMappingURL=lionrapid.umd.js.map
|
|
4820
|
+
|
|
4821
|
+
return exports;
|
|
4822
|
+
}));
|