@tolgee/core 4.7.0 → 4.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tolgee.cjs.js +2 -4
- package/dist/tolgee.cjs.js.map +1 -1
- package/dist/tolgee.cjs.min.js +1 -1
- package/dist/tolgee.cjs.min.js.map +1 -1
- package/dist/{tolgee.esm.js → tolgee.esm.min.mjs} +2 -2
- package/dist/tolgee.esm.min.mjs.map +1 -0
- package/dist/tolgee.esm.mjs +5690 -0
- package/dist/tolgee.esm.mjs.map +1 -0
- package/dist/tolgee.umd.js +2 -4
- package/dist/tolgee.umd.js.map +1 -1
- package/dist/tolgee.umd.min.js +1 -1
- package/dist/tolgee.umd.min.js.map +1 -1
- package/package.json +10 -9
- package/src/Constants/Global.ts +9 -0
- package/src/Constants/ModifierKey.ts +6 -0
- package/src/Errors/ApiHttpError.ts +8 -0
- package/src/Observer.test.ts +119 -0
- package/src/Observer.ts +68 -0
- package/src/Properties.test.ts +150 -0
- package/src/Properties.ts +112 -0
- package/src/Tolgee.test.ts +473 -0
- package/src/Tolgee.ts +335 -0
- package/src/TolgeeConfig.test.ts +21 -0
- package/src/TolgeeConfig.ts +134 -0
- package/src/__integration/FormatterIcu.test.ts +80 -0
- package/src/__integration/FormatterMissing.ts +54 -0
- package/src/__integration/Tolgee.test.ts +90 -0
- package/src/__integration/TolgeeInvisible.test.ts +145 -0
- package/src/__integration/mockTranslations.ts +6 -0
- package/src/__integration/testConfig.ts +16 -0
- package/src/__testFixtures/classMock.ts +11 -0
- package/src/__testFixtures/createElement.ts +43 -0
- package/src/__testFixtures/createTestDom.ts +25 -0
- package/src/__testFixtures/mocked.ts +25 -0
- package/src/__testFixtures/setupAfterEnv.ts +34 -0
- package/src/helpers/NodeHelper.ts +90 -0
- package/src/helpers/TextHelper.test.ts +62 -0
- package/src/helpers/TextHelper.ts +58 -0
- package/src/helpers/commonTypes.ts +8 -0
- package/src/helpers/encoderPolyfill.ts +96 -0
- package/src/helpers/secret.test.ts +61 -0
- package/src/helpers/secret.ts +68 -0
- package/src/helpers/sleep.ts +2 -0
- package/src/highlighter/HighlightFunctionsInitializer.test.ts +40 -0
- package/src/highlighter/HighlightFunctionsInitializer.ts +61 -0
- package/src/highlighter/MouseEventHandler.test.ts +151 -0
- package/src/highlighter/MouseEventHandler.ts +191 -0
- package/src/highlighter/TranslationHighlighter.test.ts +177 -0
- package/src/highlighter/TranslationHighlighter.ts +113 -0
- package/src/index.ts +10 -0
- package/src/internal.ts +2 -0
- package/src/modules/IcuFormatter.ts +17 -0
- package/src/modules/index.ts +1 -0
- package/src/services/ApiHttpService.ts +85 -0
- package/src/services/CoreService.test.ts +142 -0
- package/src/services/CoreService.ts +76 -0
- package/src/services/DependencyService.test.ts +51 -0
- package/src/services/DependencyService.ts +116 -0
- package/src/services/ElementRegistrar.test.ts +131 -0
- package/src/services/ElementRegistrar.ts +108 -0
- package/src/services/EventEmitter.ts +52 -0
- package/src/services/EventService.ts +14 -0
- package/src/services/ModuleService.ts +14 -0
- package/src/services/ScreenshotService.ts +31 -0
- package/src/services/Subscription.ts +7 -0
- package/src/services/TextService.test.ts +88 -0
- package/src/services/TextService.ts +82 -0
- package/src/services/TranslationService.test.ts +358 -0
- package/src/services/TranslationService.ts +417 -0
- package/src/services/__mocks__/CoreService.ts +17 -0
- package/src/toolsManager/Messages.test.ts +79 -0
- package/src/toolsManager/Messages.ts +60 -0
- package/src/toolsManager/PluginManager.test.ts +108 -0
- package/src/toolsManager/PluginManager.ts +129 -0
- package/src/types/DTOs.ts +25 -0
- package/src/types/apiSchema.generated.ts +6208 -0
- package/src/types.ts +146 -0
- package/src/wrappers/AbstractWrapper.ts +14 -0
- package/src/wrappers/NodeHandler.ts +143 -0
- package/src/wrappers/WrappedHandler.ts +28 -0
- package/src/wrappers/invisible/AttributeHandler.ts +23 -0
- package/src/wrappers/invisible/Coder.ts +65 -0
- package/src/wrappers/invisible/ContentHandler.ts +15 -0
- package/src/wrappers/invisible/CoreHandler.ts +17 -0
- package/src/wrappers/invisible/InvisibleWrapper.ts +59 -0
- package/src/wrappers/invisible/ValueMemory.test.ts +25 -0
- package/src/wrappers/invisible/ValueMemory.ts +16 -0
- package/src/wrappers/text/AttributeHandler.test.ts +117 -0
- package/src/wrappers/text/AttributeHandler.ts +25 -0
- package/src/wrappers/text/Coder.test.ts +298 -0
- package/src/wrappers/text/Coder.ts +202 -0
- package/src/wrappers/text/ContentHandler.test.ts +185 -0
- package/src/wrappers/text/ContentHandler.ts +21 -0
- package/src/wrappers/text/CoreHandler.test.ts +106 -0
- package/src/wrappers/text/CoreHandler.ts +45 -0
- package/src/wrappers/text/TextWrapper.ts +69 -0
- package/dist/tolgee.esm.js.map +0 -1
package/src/types.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export interface TextInputElementData {
|
|
2
|
+
oldValue: string;
|
|
3
|
+
valueInputs: string[];
|
|
4
|
+
touched: boolean;
|
|
5
|
+
oldPlaceholder: string;
|
|
6
|
+
placeholderInputs: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type TreeTranslationsData = {
|
|
10
|
+
[key: string]: string | TreeTranslationsData;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Translations = Record<string, string>;
|
|
14
|
+
export type TranslationParams = {
|
|
15
|
+
[key: string]: string | number | bigint;
|
|
16
|
+
};
|
|
17
|
+
export type TranslationParamsTags<T> = {
|
|
18
|
+
[key: string]: string | number | bigint | ((value: T | T[]) => T);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TranslateProps = {
|
|
22
|
+
key: string;
|
|
23
|
+
defaultValue?: string;
|
|
24
|
+
params?: TranslationParams;
|
|
25
|
+
noWrap?: boolean;
|
|
26
|
+
orEmpty?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type TranslatePropsTags<T> = {
|
|
29
|
+
key: string;
|
|
30
|
+
defaultValue?: string;
|
|
31
|
+
params?: TranslationParamsTags<T>;
|
|
32
|
+
noWrap?: boolean;
|
|
33
|
+
orEmpty?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type InstantProps = {
|
|
37
|
+
key: string;
|
|
38
|
+
defaultValue?: string;
|
|
39
|
+
params?: TranslationParams;
|
|
40
|
+
noWrap?: boolean;
|
|
41
|
+
orEmpty?: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type InstantPropsTags<T> = {
|
|
44
|
+
key: string;
|
|
45
|
+
defaultValue?: string;
|
|
46
|
+
params?: TranslationParamsTags<T>;
|
|
47
|
+
noWrap?: boolean;
|
|
48
|
+
orEmpty?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type KeyAndParams = {
|
|
52
|
+
key: string;
|
|
53
|
+
params: TranslationParams;
|
|
54
|
+
defaultValue?: string;
|
|
55
|
+
};
|
|
56
|
+
export type KeyAndParamsTags<T> = {
|
|
57
|
+
key: string;
|
|
58
|
+
params: TranslationParamsTags<T>;
|
|
59
|
+
defaultValue?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type TranslatedWithMetadata = {
|
|
63
|
+
translated: string;
|
|
64
|
+
key: string;
|
|
65
|
+
params: TranslationParams;
|
|
66
|
+
defaultValue: string | undefined;
|
|
67
|
+
};
|
|
68
|
+
export type TranslatedWithMetadataTags<T> = {
|
|
69
|
+
translated: TranslationTags<T>;
|
|
70
|
+
key: string;
|
|
71
|
+
params: TranslationParamsTags<T>;
|
|
72
|
+
defaultValue: string | undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type TranslationTags<T> = string | T[];
|
|
76
|
+
|
|
77
|
+
export type NodeWithMeta = Node & {
|
|
78
|
+
_tolgee: NodeMeta;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type NodeWithLock = Node & {
|
|
82
|
+
_tolgee: NodeLock;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type ElementWithMeta = Element &
|
|
86
|
+
ElementCSSInlineStyle & {
|
|
87
|
+
_tolgee: ElementMeta;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ElementMeta = {
|
|
91
|
+
wrappedWithElementOnlyKey?: string;
|
|
92
|
+
wrappedWithElementOnlyDefaultHtml?: string;
|
|
93
|
+
nodes: Set<NodeWithMeta>;
|
|
94
|
+
highlightEl?: HTMLDivElement;
|
|
95
|
+
highlight?: () => void;
|
|
96
|
+
unhighlight?: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* Stops removing of element's inactive nodes and
|
|
99
|
+
* unregistering from ElementRegistrar.
|
|
100
|
+
*
|
|
101
|
+
* It's used when user has mouse on the element, so there is
|
|
102
|
+
* potential, that element highlight will be triggered.
|
|
103
|
+
*
|
|
104
|
+
* Triggering highlight needs the metadata stored on element, so
|
|
105
|
+
* we need the ability to prevent clean.
|
|
106
|
+
*/
|
|
107
|
+
preventClean?: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type NodeMeta = {
|
|
111
|
+
oldTextContent: string;
|
|
112
|
+
keys: KeyAndParamsTags<any>[];
|
|
113
|
+
} & NodeLock;
|
|
114
|
+
|
|
115
|
+
export type NodeLock = {
|
|
116
|
+
locked?: boolean;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type Scope =
|
|
120
|
+
| 'translations.edit'
|
|
121
|
+
| 'translations.view'
|
|
122
|
+
| 'keys.edit'
|
|
123
|
+
| 'screenshots.upload'
|
|
124
|
+
| 'screenshots.view'
|
|
125
|
+
| 'screenshots.delete';
|
|
126
|
+
|
|
127
|
+
export type Mode = 'development' | 'production';
|
|
128
|
+
|
|
129
|
+
export type Unwrapped = { text: string; keys: KeyAndParamsTags<any>[] };
|
|
130
|
+
|
|
131
|
+
export interface Formatter {
|
|
132
|
+
format: FormatFunction;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface FormatterStatic {
|
|
136
|
+
type: 'formatter';
|
|
137
|
+
new (): Formatter;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type TolgeeModule = FormatterStatic;
|
|
141
|
+
|
|
142
|
+
export type FormatFunction = (props: {
|
|
143
|
+
translation: string;
|
|
144
|
+
params: Record<string, any>;
|
|
145
|
+
language: string;
|
|
146
|
+
}) => string | any[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TranslationTags, TranslationParamsTags, Unwrapped } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface AbstractWrapper {
|
|
4
|
+
handleText(node: Element): Promise<void>;
|
|
5
|
+
handleSubtree(node: Element): Promise<void>;
|
|
6
|
+
handleAttribute(node: Element): Promise<void>;
|
|
7
|
+
wrap: (
|
|
8
|
+
key: string,
|
|
9
|
+
params?: TranslationParamsTags<any>,
|
|
10
|
+
defaultValue?: string | undefined,
|
|
11
|
+
translation?: TranslationTags<any>
|
|
12
|
+
) => TranslationTags<any>;
|
|
13
|
+
unwrap(text: string): Unwrapped;
|
|
14
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { NodeHelper } from '../helpers/NodeHelper';
|
|
2
|
+
import { Properties } from '../Properties';
|
|
3
|
+
import {
|
|
4
|
+
ElementMeta,
|
|
5
|
+
ElementWithMeta,
|
|
6
|
+
KeyAndParamsTags,
|
|
7
|
+
NodeLock,
|
|
8
|
+
NodeMeta,
|
|
9
|
+
NodeWithLock,
|
|
10
|
+
NodeWithMeta,
|
|
11
|
+
} from '../types';
|
|
12
|
+
import { ElementRegistrar } from '../services/ElementRegistrar';
|
|
13
|
+
import {
|
|
14
|
+
RESTRICTED_ASCENDANT_ATTRIBUTE,
|
|
15
|
+
TOLGEE_ATTRIBUTE_NAME,
|
|
16
|
+
} from '../Constants/Global';
|
|
17
|
+
import { AbstractWrapper } from './AbstractWrapper';
|
|
18
|
+
|
|
19
|
+
export class NodeHandler {
|
|
20
|
+
constructor(
|
|
21
|
+
private properties: Properties,
|
|
22
|
+
private elementRegistrar: ElementRegistrar,
|
|
23
|
+
private wrapper: AbstractWrapper
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
static initParentElement(element: Element): ElementWithMeta {
|
|
27
|
+
if (element[TOLGEE_ATTRIBUTE_NAME] === undefined) {
|
|
28
|
+
element[TOLGEE_ATTRIBUTE_NAME] = {
|
|
29
|
+
nodes: new Set(),
|
|
30
|
+
} as ElementMeta;
|
|
31
|
+
element.setAttribute(TOLGEE_ATTRIBUTE_NAME, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return element as ElementWithMeta;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
filterRestricted<T extends Element | Text>(nodes: T[]) {
|
|
38
|
+
const restrictedElements = this.properties.config.restrictedElements;
|
|
39
|
+
return nodes.filter((n) => {
|
|
40
|
+
const e = NodeHelper.closestElement(n);
|
|
41
|
+
if (!e) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
restrictedElements.indexOf(e.tagName.toLowerCase()) === -1 &&
|
|
46
|
+
e.closest(`[${RESTRICTED_ASCENDANT_ATTRIBUTE}="true"]`) === null
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async handleNodes(nodes: Array<Text | Attr>) {
|
|
52
|
+
for (const textNode of nodes) {
|
|
53
|
+
if (textNode[TOLGEE_ATTRIBUTE_NAME] === undefined) {
|
|
54
|
+
textNode[TOLGEE_ATTRIBUTE_NAME] = {} as NodeLock;
|
|
55
|
+
}
|
|
56
|
+
const tolgeeData = textNode[TOLGEE_ATTRIBUTE_NAME] as
|
|
57
|
+
| NodeMeta
|
|
58
|
+
| NodeLock
|
|
59
|
+
| undefined;
|
|
60
|
+
if (tolgeeData?.locked !== true) {
|
|
61
|
+
this.lockNode(textNode);
|
|
62
|
+
const result = await this.wrapper.unwrap(
|
|
63
|
+
NodeHelper.getNodeText(textNode)
|
|
64
|
+
);
|
|
65
|
+
if (result) {
|
|
66
|
+
const { text, keys } = result;
|
|
67
|
+
const translatedNode = this.translateChildNode(textNode, text, keys);
|
|
68
|
+
const parentElement = this.getParentElement(translatedNode);
|
|
69
|
+
parentElement._tolgee.nodes.add(translatedNode);
|
|
70
|
+
this.elementRegistrar.register(parentElement);
|
|
71
|
+
}
|
|
72
|
+
this.unlockNode(textNode);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
translateChildNode(
|
|
78
|
+
node: Text | Attr,
|
|
79
|
+
newValue,
|
|
80
|
+
keys: KeyAndParamsTags<any>[]
|
|
81
|
+
) {
|
|
82
|
+
node[TOLGEE_ATTRIBUTE_NAME] = {
|
|
83
|
+
oldTextContent: NodeHelper.getNodeText(node),
|
|
84
|
+
keys,
|
|
85
|
+
};
|
|
86
|
+
NodeHelper.setNodeText(node, newValue);
|
|
87
|
+
return node as Node as NodeWithMeta;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private lockNode(node: Node | Attr): NodeWithLock {
|
|
91
|
+
if (node[TOLGEE_ATTRIBUTE_NAME] === undefined) {
|
|
92
|
+
node[TOLGEE_ATTRIBUTE_NAME] = {} as NodeLock;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tolgeeData = node[TOLGEE_ATTRIBUTE_NAME] as NodeMeta | NodeLock;
|
|
96
|
+
if (tolgeeData?.locked !== true) {
|
|
97
|
+
tolgeeData.locked = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return node as NodeWithLock;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private unlockNode(node: Node | Attr) {
|
|
104
|
+
if (node[TOLGEE_ATTRIBUTE_NAME]) {
|
|
105
|
+
node[TOLGEE_ATTRIBUTE_NAME].locked = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getParentElement(node: Node) {
|
|
110
|
+
const parent = this.getSuitableParent(node);
|
|
111
|
+
return NodeHandler.initParentElement(parent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private getSuitableParent(node: Node): Element {
|
|
115
|
+
const domParent = NodeHelper.getParentElement(node);
|
|
116
|
+
|
|
117
|
+
if (domParent === undefined) {
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.error(node);
|
|
120
|
+
throw new Error('No suitable parent found for node above.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!this.properties.config.passToParent) {
|
|
124
|
+
return domParent;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(this.properties.config.passToParent)) {
|
|
128
|
+
const tagNameEquals = (elementTagName: string) =>
|
|
129
|
+
domParent.tagName.toLowerCase() === elementTagName.toLowerCase();
|
|
130
|
+
if (this.properties.config.passToParent.findIndex(tagNameEquals) === -1) {
|
|
131
|
+
return domParent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof this.properties.config.passToParent === 'function') {
|
|
136
|
+
if (!this.properties.config.passToParent(domParent)) {
|
|
137
|
+
return domParent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.getSuitableParent(domParent);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NodeHelper } from '../helpers/NodeHelper';
|
|
2
|
+
import { NodeHandler } from './NodeHandler';
|
|
3
|
+
import { ElementRegistrar } from '../services/ElementRegistrar';
|
|
4
|
+
import { TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE } from '../Constants/Global';
|
|
5
|
+
|
|
6
|
+
export class WrappedHandler {
|
|
7
|
+
constructor(
|
|
8
|
+
private elementRegistrar: ElementRegistrar,
|
|
9
|
+
private nodeHandler: NodeHandler
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async handle(node: Node): Promise<void> {
|
|
13
|
+
const xPath = `./descendant-or-self::*[@${TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE}]`;
|
|
14
|
+
const nodes = NodeHelper.evaluate(xPath, node);
|
|
15
|
+
const filtered: Element[] = this.nodeHandler.filterRestricted(
|
|
16
|
+
nodes as Element[]
|
|
17
|
+
);
|
|
18
|
+
filtered.forEach((element) => {
|
|
19
|
+
const elementWithMeta = NodeHandler.initParentElement(element);
|
|
20
|
+
elementWithMeta._tolgee.wrappedWithElementOnlyKey = element.getAttribute(
|
|
21
|
+
TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE
|
|
22
|
+
);
|
|
23
|
+
elementWithMeta._tolgee.wrappedWithElementOnlyDefaultHtml =
|
|
24
|
+
element.innerHTML;
|
|
25
|
+
this.elementRegistrar.register(elementWithMeta);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NodeHandler } from '../NodeHandler';
|
|
2
|
+
import { Properties } from '../../Properties';
|
|
3
|
+
import { NodeHelper } from '../../helpers/NodeHelper';
|
|
4
|
+
import { INVISIBLE_CHARACTERS } from '../../helpers/secret';
|
|
5
|
+
|
|
6
|
+
export class AttributeHandler {
|
|
7
|
+
constructor(
|
|
8
|
+
private properties: Properties,
|
|
9
|
+
private nodeHandler: NodeHandler
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async handle(node: Element) {
|
|
13
|
+
for (const [tag, attributes] of Object.entries(
|
|
14
|
+
this.properties.config.tagAttributes
|
|
15
|
+
)) {
|
|
16
|
+
for (const attribute of attributes) {
|
|
17
|
+
const expression = `descendant-or-self::${tag}/@${attribute}[contains(., '${INVISIBLE_CHARACTERS[0]}')]`;
|
|
18
|
+
const nodes: Array<Attr | Text> = NodeHelper.evaluate(expression, node);
|
|
19
|
+
await this.nodeHandler.handleNodes(nodes);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeFromText,
|
|
3
|
+
encodeMessage,
|
|
4
|
+
removeSecrets,
|
|
5
|
+
stringToCodePoints,
|
|
6
|
+
} from '../../helpers/secret';
|
|
7
|
+
import {
|
|
8
|
+
KeyAndParams,
|
|
9
|
+
TranslationTags,
|
|
10
|
+
TranslationParamsTags,
|
|
11
|
+
Unwrapped,
|
|
12
|
+
} from '../../types';
|
|
13
|
+
import { ValueMemory } from './ValueMemory';
|
|
14
|
+
|
|
15
|
+
export class Coder {
|
|
16
|
+
private keyMemory = new ValueMemory();
|
|
17
|
+
private defaultMemory = new ValueMemory();
|
|
18
|
+
|
|
19
|
+
unwrap(text: string): Unwrapped {
|
|
20
|
+
const keysAndParams = [] as KeyAndParams[];
|
|
21
|
+
const messages = decodeFromText(text);
|
|
22
|
+
|
|
23
|
+
messages.forEach((msg) => {
|
|
24
|
+
const [keyCode, defaultCode] = stringToCodePoints(msg);
|
|
25
|
+
const key = this.keyMemory.numberToValue(keyCode);
|
|
26
|
+
const defaultValue =
|
|
27
|
+
defaultCode !== undefined
|
|
28
|
+
? this.defaultMemory.numberToValue(defaultCode)
|
|
29
|
+
: undefined;
|
|
30
|
+
keysAndParams.push({
|
|
31
|
+
key: key,
|
|
32
|
+
params: undefined,
|
|
33
|
+
defaultValue,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = removeSecrets(text);
|
|
38
|
+
|
|
39
|
+
if (keysAndParams.length) {
|
|
40
|
+
return { text: result, keys: keysAndParams };
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public wrap(
|
|
46
|
+
key: string,
|
|
47
|
+
_params: TranslationParamsTags<any> = {},
|
|
48
|
+
defaultValue: string | undefined = undefined,
|
|
49
|
+
translation: string | any[] | undefined = undefined
|
|
50
|
+
): TranslationTags<any> {
|
|
51
|
+
const codes = [this.keyMemory.valueToNumber(key)];
|
|
52
|
+
if (defaultValue) {
|
|
53
|
+
codes.push(this.defaultMemory.valueToNumber(defaultValue));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const value = translation || '';
|
|
57
|
+
const invisibleMark = encodeMessage(String.fromCodePoint(...codes));
|
|
58
|
+
|
|
59
|
+
return typeof value === 'string'
|
|
60
|
+
? value + invisibleMark
|
|
61
|
+
: Array.isArray(value)
|
|
62
|
+
? [...value, invisibleMark]
|
|
63
|
+
: [value, invisibleMark];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NodeHelper } from '../../helpers/NodeHelper';
|
|
2
|
+
import { INVISIBLE_CHARACTERS } from '../../helpers/secret';
|
|
3
|
+
import { NodeHandler } from '../NodeHandler';
|
|
4
|
+
|
|
5
|
+
export class ContentHandler {
|
|
6
|
+
constructor(private nodeHandler: NodeHandler) {}
|
|
7
|
+
|
|
8
|
+
async handle(node: Node): Promise<void> {
|
|
9
|
+
const xPath = `./descendant-or-self::text()[contains(., '${INVISIBLE_CHARACTERS[0]}')]`;
|
|
10
|
+
const nodes = NodeHelper.evaluate(xPath, node);
|
|
11
|
+
const filtered: Text[] = this.nodeHandler.filterRestricted(nodes as Text[]);
|
|
12
|
+
|
|
13
|
+
await this.nodeHandler.handleNodes(filtered);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ContentHandler } from './ContentHandler';
|
|
2
|
+
import { AttributeHandler } from './AttributeHandler';
|
|
3
|
+
import { WrappedHandler } from '../WrappedHandler';
|
|
4
|
+
|
|
5
|
+
export class CoreHandler {
|
|
6
|
+
constructor(
|
|
7
|
+
private textHandler: ContentHandler,
|
|
8
|
+
private attributeHandler: AttributeHandler,
|
|
9
|
+
private wrappedHandler: WrappedHandler
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
public async handleSubtree(target: Element) {
|
|
13
|
+
await this.attributeHandler.handle(target);
|
|
14
|
+
await this.textHandler.handle(target);
|
|
15
|
+
await this.wrappedHandler.handle(target);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Properties } from '../../Properties';
|
|
2
|
+
import { ElementRegistrar } from '../../services/ElementRegistrar';
|
|
3
|
+
import { TranslationParams } from '../../types';
|
|
4
|
+
import { NodeHandler } from '../NodeHandler';
|
|
5
|
+
import { AttributeHandler } from './AttributeHandler';
|
|
6
|
+
import { Coder } from './Coder';
|
|
7
|
+
import { CoreHandler } from './CoreHandler';
|
|
8
|
+
import { ContentHandler } from './ContentHandler';
|
|
9
|
+
import { WrappedHandler } from '../WrappedHandler';
|
|
10
|
+
import { AbstractWrapper } from '../AbstractWrapper';
|
|
11
|
+
|
|
12
|
+
export class InvisibleWrapper implements AbstractWrapper {
|
|
13
|
+
private coder: Coder;
|
|
14
|
+
private coreHandler: CoreHandler;
|
|
15
|
+
private attributeHandler: AttributeHandler;
|
|
16
|
+
private wrappedHandler: WrappedHandler;
|
|
17
|
+
private textHandler: ContentHandler;
|
|
18
|
+
private nodeHandler: any;
|
|
19
|
+
constructor(properties: Properties, elementRegistrar: ElementRegistrar) {
|
|
20
|
+
this.coder = new Coder();
|
|
21
|
+
this.nodeHandler = new NodeHandler(properties, elementRegistrar, this);
|
|
22
|
+
this.textHandler = new ContentHandler(this.nodeHandler);
|
|
23
|
+
this.attributeHandler = new AttributeHandler(properties, this.nodeHandler);
|
|
24
|
+
this.wrappedHandler = new WrappedHandler(
|
|
25
|
+
elementRegistrar,
|
|
26
|
+
this.nodeHandler
|
|
27
|
+
);
|
|
28
|
+
this.coreHandler = new CoreHandler(
|
|
29
|
+
this.textHandler,
|
|
30
|
+
this.attributeHandler,
|
|
31
|
+
this.wrappedHandler
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public handleText(node: Element) {
|
|
36
|
+
return this.textHandler.handle(node);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public handleSubtree(node: Element) {
|
|
40
|
+
return this.coreHandler.handleSubtree(node);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public handleAttribute(node: Element) {
|
|
44
|
+
return this.attributeHandler.handle(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public wrap(
|
|
48
|
+
key: string,
|
|
49
|
+
params: TranslationParams = {},
|
|
50
|
+
defaultValue: string | undefined = undefined,
|
|
51
|
+
translation = ''
|
|
52
|
+
) {
|
|
53
|
+
return this.coder.wrap(key, params, defaultValue, translation);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public unwrap(text: string) {
|
|
57
|
+
return this.coder.unwrap(text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
jest.dontMock('./ValueMemory');
|
|
2
|
+
import { ValueMemory } from './ValueMemory';
|
|
3
|
+
|
|
4
|
+
describe('Value memory', () => {
|
|
5
|
+
let valueMemory: ValueMemory;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
valueMemory = new ValueMemory();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('stores key correctly', () => {
|
|
12
|
+
const num = valueMemory.valueToNumber('abcd');
|
|
13
|
+
expect(num).toEqual(0);
|
|
14
|
+
expect(valueMemory.valueToNumber('abcd')).toEqual(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('retrieves key correctly', () => {
|
|
18
|
+
const num = valueMemory.valueToNumber('abcd');
|
|
19
|
+
expect(valueMemory.numberToValue(num)).toEqual('abcd');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('retrieves missing key correctly', () => {
|
|
23
|
+
expect(valueMemory.numberToValue(100)).toEqual(undefined);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class ValueMemory {
|
|
2
|
+
private values: string[] = [];
|
|
3
|
+
|
|
4
|
+
public valueToNumber(key: string) {
|
|
5
|
+
let index = this.values.indexOf(key);
|
|
6
|
+
if (index === -1) {
|
|
7
|
+
index = this.values.length;
|
|
8
|
+
this.values.push(key);
|
|
9
|
+
}
|
|
10
|
+
return index;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public numberToValue(num: number) {
|
|
14
|
+
return this.values[num];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
jest.dontMock('./AttributeHandler');
|
|
2
|
+
jest.dontMock('./TextWrapper');
|
|
3
|
+
jest.dontMock('../NodeHandler');
|
|
4
|
+
jest.dontMock('../../services/EventService');
|
|
5
|
+
jest.dontMock('../../helpers/NodeHelper.ts');
|
|
6
|
+
jest.dontMock('../../services/DependencyService');
|
|
7
|
+
jest.dontMock('../../TolgeeConfig');
|
|
8
|
+
|
|
9
|
+
import { Properties } from '../../Properties';
|
|
10
|
+
import { AttributeHandler } from './AttributeHandler';
|
|
11
|
+
import { ElementWithMeta, NodeWithMeta, Unwrapped } from '../../types';
|
|
12
|
+
import { NodeHelper } from '../../helpers/NodeHelper';
|
|
13
|
+
import { createTestDom } from '@testFixtures/createTestDom';
|
|
14
|
+
import { getMockedInstance } from '@testFixtures/mocked';
|
|
15
|
+
import { ElementRegistrar } from '../../services/ElementRegistrar';
|
|
16
|
+
import { DependencyService } from '../../services/DependencyService';
|
|
17
|
+
import { Coder } from './Coder';
|
|
18
|
+
|
|
19
|
+
describe('AttributeHandler', () => {
|
|
20
|
+
let attributeHandler: AttributeHandler;
|
|
21
|
+
|
|
22
|
+
const mockedKeys = [
|
|
23
|
+
{
|
|
24
|
+
key: 'dummyKey',
|
|
25
|
+
params: { dummyParam: 'dummyValue' },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const mockedTranslateInner = (text) => {
|
|
30
|
+
return {
|
|
31
|
+
text: text.replace(/{{(.*?)}}/gs, '$1'),
|
|
32
|
+
keys: mockedKeys,
|
|
33
|
+
} as Unwrapped;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const gv = (key) => mockedTranslateInner(key).text;
|
|
37
|
+
const mockedTranslate = jest.fn(mockedTranslateInner);
|
|
38
|
+
let c: ReturnType<typeof createTestDom>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
c = createTestDom(document);
|
|
42
|
+
const dependencyService = new DependencyService();
|
|
43
|
+
dependencyService.init({});
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
attributeHandler = dependencyService.wrapper.attributeHandler;
|
|
46
|
+
getMockedInstance(Properties).config = {
|
|
47
|
+
inputPrefix: '{{',
|
|
48
|
+
inputSuffix: '}}',
|
|
49
|
+
restrictedElements: [],
|
|
50
|
+
tagAttributes: {
|
|
51
|
+
'*': ['aria-label'],
|
|
52
|
+
},
|
|
53
|
+
passToParent: ['option'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
getMockedInstance(Coder).unwrap = (...args) => mockedTranslate(...args);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
jest.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('in production mode', () => {
|
|
64
|
+
let xPath: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
await attributeHandler.handle(document.body);
|
|
68
|
+
xPath = `./div/div/@aria-label[contains(., '${gv(c.ariaLabelKey)}')]`;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Can be created', () => {
|
|
72
|
+
expect(attributeHandler).not.toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('will handle text in aria-label attribute of div', async () => {
|
|
76
|
+
expect(xPath).toBeFoundIn(document.body);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Node's _tolgee property", () => {
|
|
80
|
+
let node: NodeWithMeta;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
node = NodeHelper.evaluateToSingle(
|
|
84
|
+
xPath,
|
|
85
|
+
document.body
|
|
86
|
+
) as NodeWithMeta;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('will be defined', () => {
|
|
90
|
+
expect(node._tolgee).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Parent element's _tolgee property", () => {
|
|
95
|
+
let element: ElementWithMeta;
|
|
96
|
+
let node: NodeWithMeta;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
node = NodeHelper.evaluateToSingle(xPath, document.body);
|
|
100
|
+
element = (node as any as Attr).ownerElement as any as ElementWithMeta;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('will be defined', () => {
|
|
104
|
+
expect(element._tolgee).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('will register the node', async () => {
|
|
110
|
+
await attributeHandler.handle(document.body);
|
|
111
|
+
const xPath = `./div/div/@aria-label[contains(., '${gv(c.ariaLabelKey)}')]`;
|
|
112
|
+
const node = NodeHelper.evaluateToSingle(xPath, document.body) as Attr;
|
|
113
|
+
expect(getMockedInstance(ElementRegistrar).register).toBeCalledWith(
|
|
114
|
+
node.ownerElement
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|