@tolgee/web 4.9.3-rc.d287ae9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/tolgee-backend-fetch.cjs.min.js +2 -0
  4. package/dist/tolgee-backend-fetch.cjs.min.js.map +1 -0
  5. package/dist/tolgee-backend-fetch.esm.min.mjs +2 -0
  6. package/dist/tolgee-backend-fetch.esm.min.mjs.map +1 -0
  7. package/dist/tolgee-backend-fetch.umd.min.js +2 -0
  8. package/dist/tolgee-backend-fetch.umd.min.js.map +1 -0
  9. package/dist/tolgee-context-ui.cjs.min.js +143 -0
  10. package/dist/tolgee-context-ui.cjs.min.js.map +1 -0
  11. package/dist/tolgee-context-ui.esm.min.mjs +143 -0
  12. package/dist/tolgee-context-ui.esm.min.mjs.map +1 -0
  13. package/dist/tolgee-context-ui.umd.min.js +143 -0
  14. package/dist/tolgee-context-ui.umd.min.js.map +1 -0
  15. package/dist/tolgee-in-context-tools.cjs.min.js +143 -0
  16. package/dist/tolgee-in-context-tools.cjs.min.js.map +1 -0
  17. package/dist/tolgee-in-context-tools.esm.min.mjs +143 -0
  18. package/dist/tolgee-in-context-tools.esm.min.mjs.map +1 -0
  19. package/dist/tolgee-in-context-tools.umd.min.js +143 -0
  20. package/dist/tolgee-in-context-tools.umd.min.js.map +1 -0
  21. package/dist/tolgee-invisible-observer.cjs.min.js +2 -0
  22. package/dist/tolgee-invisible-observer.cjs.min.js.map +1 -0
  23. package/dist/tolgee-invisible-observer.esm.min.mjs +2 -0
  24. package/dist/tolgee-invisible-observer.esm.min.mjs.map +1 -0
  25. package/dist/tolgee-invisible-observer.umd.min.js +2 -0
  26. package/dist/tolgee-invisible-observer.umd.min.js.map +1 -0
  27. package/dist/tolgee-language-detector.cjs.min.js +2 -0
  28. package/dist/tolgee-language-detector.cjs.min.js.map +1 -0
  29. package/dist/tolgee-language-detector.esm.min.mjs +2 -0
  30. package/dist/tolgee-language-detector.esm.min.mjs.map +1 -0
  31. package/dist/tolgee-language-detector.umd.min.js +2 -0
  32. package/dist/tolgee-language-detector.umd.min.js.map +1 -0
  33. package/dist/tolgee-language-storage.cjs.min.js +2 -0
  34. package/dist/tolgee-language-storage.cjs.min.js.map +1 -0
  35. package/dist/tolgee-language-storage.esm.min.mjs +2 -0
  36. package/dist/tolgee-language-storage.esm.min.mjs.map +1 -0
  37. package/dist/tolgee-language-storage.umd.min.js +2 -0
  38. package/dist/tolgee-language-storage.umd.min.js.map +1 -0
  39. package/dist/tolgee-text-observer.cjs.min.js +2 -0
  40. package/dist/tolgee-text-observer.cjs.min.js.map +1 -0
  41. package/dist/tolgee-text-observer.esm.min.mjs +2 -0
  42. package/dist/tolgee-text-observer.esm.min.mjs.map +1 -0
  43. package/dist/tolgee-text-observer.umd.min.js +2 -0
  44. package/dist/tolgee-text-observer.umd.min.js.map +1 -0
  45. package/dist/tolgee-web-tolgee.cjs.min.js +2 -0
  46. package/dist/tolgee-web-tolgee.cjs.min.js.map +1 -0
  47. package/dist/tolgee-web-tolgee.esm.min.mjs +2 -0
  48. package/dist/tolgee-web-tolgee.esm.min.mjs.map +1 -0
  49. package/dist/tolgee-web-tolgee.umd.min.js +2 -0
  50. package/dist/tolgee-web-tolgee.umd.min.js.map +1 -0
  51. package/dist/tolgee-web.cjs.js +26921 -0
  52. package/dist/tolgee-web.cjs.js.map +1 -0
  53. package/dist/tolgee-web.cjs.min.js +143 -0
  54. package/dist/tolgee-web.cjs.min.js.map +1 -0
  55. package/dist/tolgee-web.esm.min.mjs +143 -0
  56. package/dist/tolgee-web.esm.min.mjs.map +1 -0
  57. package/dist/tolgee-web.esm.mjs +26899 -0
  58. package/dist/tolgee-web.esm.mjs.map +1 -0
  59. package/dist/tolgee-web.umd.js +26927 -0
  60. package/dist/tolgee-web.umd.js.map +1 -0
  61. package/dist/tolgee-web.umd.min.js +143 -0
  62. package/dist/tolgee-web.umd.min.js.map +1 -0
  63. package/lib/BackendFetch.d.ts +4 -0
  64. package/lib/BrowserExtensionPlugin/BrowserExtensionPlugin.d.ts +8 -0
  65. package/lib/BrowserExtensionPlugin/constants.d.ts +3 -0
  66. package/lib/BrowserExtensionPlugin/loadInContextLib.d.ts +1 -0
  67. package/lib/ContextUi.d.ts +2 -0
  68. package/lib/DevBackend.d.ts +2 -0
  69. package/lib/DevTools.d.ts +3 -0
  70. package/lib/InContextTools.d.ts +2 -0
  71. package/lib/InvisibleObserver.d.ts +2 -0
  72. package/lib/LanguageDetector.d.ts +3 -0
  73. package/lib/LanguageStorage.d.ts +3 -0
  74. package/lib/TextObserver.d.ts +2 -0
  75. package/lib/WebTolgee.d.ts +3 -0
  76. package/lib/index.d.ts +4 -0
  77. package/lib/observers/general/DomHelper.d.ts +4 -0
  78. package/lib/observers/general/ElementHighlighter.d.ts +10 -0
  79. package/lib/observers/general/ElementMeta.d.ts +3 -0
  80. package/lib/observers/general/ElementRegistry.d.ts +11 -0
  81. package/lib/observers/general/ElementStore.d.ts +10 -0
  82. package/lib/observers/general/GeneralObserver.d.ts +12 -0
  83. package/lib/observers/general/MouseEventHandler.d.ts +13 -0
  84. package/lib/observers/general/NodeHandler.d.ts +6 -0
  85. package/lib/observers/general/helpers.d.ts +7 -0
  86. package/lib/observers/invisible/InvisibleWrapper.d.ts +2 -0
  87. package/lib/observers/invisible/ValueMemory.d.ts +5 -0
  88. package/lib/observers/invisible/encoderPolyfill.d.ts +8 -0
  89. package/lib/observers/invisible/secret.d.ts +6 -0
  90. package/lib/observers/text/TextWrapper.d.ts +8 -0
  91. package/lib/observers/text/helpers.d.ts +3 -0
  92. package/lib/tools/decodeApiKey.d.ts +1 -0
  93. package/lib/tools/extension.d.ts +28 -0
  94. package/lib/typedIndex.d.ts +11 -0
  95. package/lib/types.d.ts +28 -0
  96. package/lib/ui/KeyContextMenu/KeyContextMenu.d.ts +19 -0
  97. package/lib/ui/KeyDialog/KeyDialog.d.ts +23 -0
  98. package/lib/ui/KeyDialog/KeyForm.d.ts +2 -0
  99. package/lib/ui/KeyDialog/LanguageSelect.d.ts +2 -0
  100. package/lib/ui/KeyDialog/NewWindow.d.ts +2 -0
  101. package/lib/ui/KeyDialog/NsSelect.d.ts +9 -0
  102. package/lib/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.d.ts +6 -0
  103. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.d.ts +8 -0
  104. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.d.ts +6 -0
  105. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.d.ts +2 -0
  106. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.d.ts +8 -0
  107. package/lib/ui/KeyDialog/ScreenshotGallery/utils.d.ts +3 -0
  108. package/lib/ui/KeyDialog/TranslationDialog.d.ts +2 -0
  109. package/lib/ui/KeyDialog/TranslationDialogContextProvider.d.ts +128 -0
  110. package/lib/ui/KeyDialog/TranslationDialogWrapper.d.ts +2 -0
  111. package/lib/ui/KeyDialog/TranslationFields.d.ts +2 -0
  112. package/lib/ui/KeyDialog/languageHelpers.d.ts +12 -0
  113. package/lib/ui/KeyDialog/tools.d.ts +3 -0
  114. package/lib/ui/ThemeProvider.d.ts +2 -0
  115. package/lib/ui/client/QueryProvider.d.ts +12 -0
  116. package/lib/ui/client/apiSchema.generated.d.ts +3283 -0
  117. package/lib/ui/client/client.d.ts +5 -0
  118. package/lib/ui/client/types.d.ts +58 -0
  119. package/lib/ui/client/useQueryApi.d.ts +84 -0
  120. package/lib/ui/common/BodyEnd.d.ts +13 -0
  121. package/lib/ui/common/FieldTitle.d.ts +2 -0
  122. package/lib/ui/common/LoadingButton.d.ts +7 -0
  123. package/lib/ui/constants.d.ts +5 -0
  124. package/lib/ui/index.d.ts +13 -0
  125. package/lib/ui/tools/createProvider.d.ts +5 -0
  126. package/lib/ui/tools/isLanguagePermitted.d.ts +1 -0
  127. package/lib/ui/tools/sleep.d.ts +1 -0
  128. package/package.json +91 -0
  129. package/src/BackendFetch.ts +64 -0
  130. package/src/BrowserExtensionPlugin/BrowserExtensionPlugin.ts +98 -0
  131. package/src/BrowserExtensionPlugin/constants.ts +3 -0
  132. package/src/BrowserExtensionPlugin/loadInContextLib.ts +32 -0
  133. package/src/ContextUi.ts +7 -0
  134. package/src/DevBackend.ts +30 -0
  135. package/src/DevTools.ts +9 -0
  136. package/src/InContextTools.ts +20 -0
  137. package/src/InvisibleObserver.ts +16 -0
  138. package/src/LanguageDetector.test.ts +19 -0
  139. package/src/LanguageDetector.ts +32 -0
  140. package/src/LanguageStorage.ts +23 -0
  141. package/src/TextObserver.ts +45 -0
  142. package/src/WebTolgee.ts +8 -0
  143. package/src/__test__/browser.extension.test.ts +69 -0
  144. package/src/__test__/observer.test.ts +13 -0
  145. package/src/__test__/testObserver.ts +106 -0
  146. package/src/__test__/testRetranslate.ts +47 -0
  147. package/src/index.ts +4 -0
  148. package/src/observers/general/DomHelper.ts +46 -0
  149. package/src/observers/general/ElementHighlighter.ts +72 -0
  150. package/src/observers/general/ElementMeta.ts +17 -0
  151. package/src/observers/general/ElementRegistry.ts +159 -0
  152. package/src/observers/general/ElementStore.ts +34 -0
  153. package/src/observers/general/GeneralObserver.ts +133 -0
  154. package/src/observers/general/MouseEventHandler.ts +198 -0
  155. package/src/observers/general/NodeHandler.ts +39 -0
  156. package/src/observers/general/helpers.ts +65 -0
  157. package/src/observers/invisible/InvisibleWrapper.test.ts +17 -0
  158. package/src/observers/invisible/InvisibleWrapper.ts +96 -0
  159. package/src/observers/invisible/ValueMemory.test.ts +25 -0
  160. package/src/observers/invisible/ValueMemory.ts +20 -0
  161. package/src/observers/invisible/encoderPolyfill.ts +96 -0
  162. package/src/observers/invisible/secret.test.ts +61 -0
  163. package/src/observers/invisible/secret.ts +68 -0
  164. package/src/observers/text/TextWrapper.ts +258 -0
  165. package/src/observers/text/helpers.ts +56 -0
  166. package/src/tools/decodeApiKey.test.ts +14 -0
  167. package/src/tools/decodeApiKey.ts +74 -0
  168. package/src/tools/extension.test.ts +159 -0
  169. package/src/tools/extension.ts +117 -0
  170. package/src/typedIndex.ts +13 -0
  171. package/src/types.ts +33 -0
  172. package/src/ui/KeyContextMenu/KeyContextMenu.tsx +106 -0
  173. package/src/ui/KeyDialog/KeyDialog.tsx +67 -0
  174. package/src/ui/KeyDialog/KeyForm.tsx +208 -0
  175. package/src/ui/KeyDialog/LanguageSelect.tsx +78 -0
  176. package/src/ui/KeyDialog/NewWindow.tsx +106 -0
  177. package/src/ui/KeyDialog/NsSelect.tsx +67 -0
  178. package/src/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.tsx +97 -0
  179. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.tsx +33 -0
  180. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.tsx +138 -0
  181. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.tsx +240 -0
  182. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.tsx +113 -0
  183. package/src/ui/KeyDialog/ScreenshotGallery/utils.ts +17 -0
  184. package/src/ui/KeyDialog/TranslationDialog.tsx +14 -0
  185. package/src/ui/KeyDialog/TranslationDialogContextProvider.tsx +464 -0
  186. package/src/ui/KeyDialog/TranslationDialogWrapper.tsx +44 -0
  187. package/src/ui/KeyDialog/TranslationFields.tsx +113 -0
  188. package/src/ui/KeyDialog/languageHelpers.ts +18 -0
  189. package/src/ui/KeyDialog/tools.ts +30 -0
  190. package/src/ui/ThemeProvider.tsx +71 -0
  191. package/src/ui/__test__/keyContextMenu.test.ts +56 -0
  192. package/src/ui/client/QueryProvider.tsx +38 -0
  193. package/src/ui/client/apiSchema.generated.ts +3281 -0
  194. package/src/ui/client/client.ts +155 -0
  195. package/src/ui/client/types.ts +113 -0
  196. package/src/ui/client/useQueryApi.ts +121 -0
  197. package/src/ui/common/BodyEnd.tsx +44 -0
  198. package/src/ui/common/FieldTitle.tsx +9 -0
  199. package/src/ui/common/LoadingButton.tsx +45 -0
  200. package/src/ui/constants.ts +12 -0
  201. package/src/ui/index.ts +88 -0
  202. package/src/ui/screenshots/ScreenshotPreview.tsx +18 -0
  203. package/src/ui/tools/createProvider.tsx +54 -0
  204. package/src/ui/tools/isLanguagePermitted.ts +14 -0
  205. package/src/ui/tools/sleep.ts +2 -0
  206. package/types/index.d.ts +9 -0
@@ -0,0 +1,61 @@
1
+ jest.disableAutomock();
2
+
3
+ import {
4
+ decodeFromText,
5
+ encodeMessage,
6
+ INVISIBLE_CHARACTERS,
7
+ removeSecrets,
8
+ stringToCodePoints,
9
+ } from './secret';
10
+
11
+ describe('Invisible encoder/decoder', () => {
12
+ it('Works with simple text', () => {
13
+ const message = 'Tolgee' + encodeMessage('secret');
14
+ expect(decodeFromText(message)).toEqual(['secret']);
15
+ });
16
+
17
+ it('Works with utf-8 text', () => {
18
+ const message = '💩💩💩' + encodeMessage('💩💩');
19
+ expect(decodeFromText(message)).toEqual(['💩💩']);
20
+ });
21
+
22
+ it('Works with joiners in text', () => {
23
+ const message = '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage('💩💩');
24
+ expect(decodeFromText(message)).toEqual(['💩💩']);
25
+ });
26
+
27
+ it('Removes secrets correctly', () => {
28
+ const message = '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage('💩💩');
29
+ const cleanedMessage = removeSecrets(message);
30
+ expect(cleanedMessage).toEqual('👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦');
31
+ expect(decodeFromText(cleanedMessage)).toEqual([]);
32
+ });
33
+
34
+ it('works correctly with numbers', () => {
35
+ [...Array(3)].map((_, i) => {
36
+ const numbers = [i, i + 10, i + 100, i + 1_000, i + 10_000, i + 100_000];
37
+ const message =
38
+ '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage(String.fromCodePoint(...numbers));
39
+ expect(decodeFromText(message)).toEqual([
40
+ String.fromCodePoint(...numbers),
41
+ ]);
42
+ });
43
+ });
44
+
45
+ it('works with unicode numbers', () => {
46
+ const message =
47
+ '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage(String.fromCodePoint(10, 500_000, 1_000_000));
48
+ const decodedString = decodeFromText(message)[0];
49
+ expect(stringToCodePoints(decodedString)).toEqual([10, 500_000, 1_000_000]);
50
+ });
51
+
52
+ it('works correctly with multiple non-joiners in row', () => {
53
+ // valid secret byte is 8 + 1 (non-joiner), so 8 should be ignored
54
+ const originalMessage = `a${INVISIBLE_CHARACTERS[0].repeat(8)}bcd`;
55
+ const message = originalMessage + encodeMessage('💩💩');
56
+ const cleanedMessage = removeSecrets(message);
57
+ expect(decodeFromText(message)).toEqual(['💩💩']);
58
+ expect(decodeFromText(cleanedMessage)).toEqual([]);
59
+ expect(cleanedMessage).toEqual(originalMessage);
60
+ });
61
+ });
@@ -0,0 +1,68 @@
1
+ import { Encoder, Decoder } from './encoderPolyfill';
2
+
3
+ export const INVISIBLE_CHARACTERS = ['\u200C', '\u200D'];
4
+
5
+ export const INVISIBLE_REGEX = RegExp(
6
+ `([${INVISIBLE_CHARACTERS.join('')}]{9})+`,
7
+ 'gu'
8
+ );
9
+
10
+ const toBytes = (text: string) => {
11
+ return Array.from(new Encoder().encode(text));
12
+ };
13
+
14
+ const fromBytes = (bytes: Iterable<number>) => {
15
+ return new Decoder().decode(new Uint8Array(bytes));
16
+ };
17
+
18
+ const padToWholeBytes = (binary: string) => {
19
+ const needsToAdd = 8 - binary.length;
20
+ return '0'.repeat(needsToAdd) + binary;
21
+ };
22
+
23
+ export const encodeMessage = (text: string) => {
24
+ const bytes = toBytes(text).map(Number);
25
+ const binary = bytes
26
+ .map((byte) => padToWholeBytes(byte.toString(2)) + '0')
27
+ .join('');
28
+
29
+ const result = Array.from(binary)
30
+ .map((b) => INVISIBLE_CHARACTERS[Number(b)])
31
+ .join('');
32
+
33
+ return result;
34
+ };
35
+
36
+ const decodeMessage = (message: string) => {
37
+ const binary = Array.from(message)
38
+ .map((character) => {
39
+ return INVISIBLE_CHARACTERS.indexOf(character);
40
+ })
41
+ .map(String)
42
+ .join('');
43
+
44
+ const textBytes = binary.match(/(.{9})/g);
45
+ const codes = Uint8Array.from(
46
+ textBytes?.map((byte) => parseInt(byte.slice(0, 8), 2)) || []
47
+ );
48
+ return fromBytes(codes);
49
+ };
50
+
51
+ export const decodeFromText = (text: string) => {
52
+ const invisibleMessages = text
53
+ .match(INVISIBLE_REGEX)
54
+ ?.filter((m) => m.length > 8);
55
+ return invisibleMessages?.map(decodeMessage) || [];
56
+ };
57
+
58
+ export const removeSecrets = (text: string) => {
59
+ return text.replace(INVISIBLE_REGEX, '');
60
+ };
61
+
62
+ export const stringToCodePoints = (text: string) => {
63
+ const result: number[] = [];
64
+ for (const codePoint of text) {
65
+ result.push(codePoint.codePointAt(0)!);
66
+ }
67
+ return result;
68
+ };
@@ -0,0 +1,258 @@
1
+ import type {
2
+ KeyAndParams,
3
+ TranslatePropsInternal,
4
+ WrapperAttributeXPathGetter,
5
+ WrapperInterface,
6
+ WrapperUnwrapFunction,
7
+ WrapperWrapFunction,
8
+ } from '@tolgee/core';
9
+ import { isCharEscaped } from './helpers';
10
+
11
+ type Props = {
12
+ inputPrefix: string;
13
+ inputSuffix: string;
14
+ translate: (params: TranslatePropsInternal) => string;
15
+ };
16
+
17
+ export const TextWrapper = ({
18
+ inputPrefix,
19
+ inputSuffix,
20
+ translate,
21
+ }: Props): WrapperInterface => {
22
+ function getRawUnWrapRegex(): string {
23
+ const escapedPrefix = escapeForRegExp(inputPrefix);
24
+ const escapedSuffix = escapeForRegExp(inputSuffix);
25
+ return `(\\\\?)(${escapedPrefix}(.*?)${escapedSuffix})`;
26
+ }
27
+
28
+ function parseUnwrapped(unwrappedString: string): KeyAndParams {
29
+ let escaped = false;
30
+ let actual = '';
31
+ let paramName = '';
32
+ let readingState:
33
+ | 'KEY'
34
+ | 'DEFAULT_VALUE'
35
+ | 'PARAM_NAME'
36
+ | 'PARAM_VALUE'
37
+ | 'NAMESPACE' = 'KEY';
38
+
39
+ const result = {
40
+ key: '',
41
+ ns: undefined as string[] | undefined,
42
+ params: {},
43
+ defaultValue: undefined as string | undefined,
44
+ } as KeyAndParams;
45
+
46
+ const addNamespace = (ns: string) => {
47
+ if (!result.ns) {
48
+ result.ns = [];
49
+ }
50
+ (result.ns as string[]).push(ns);
51
+ };
52
+
53
+ for (const char of unwrappedString) {
54
+ if (char === '\\' && !escaped) {
55
+ escaped = true;
56
+ continue;
57
+ }
58
+ if (escaped) {
59
+ escaped = false;
60
+ actual += char;
61
+ continue;
62
+ }
63
+ if (readingState === 'KEY' && char === ',') {
64
+ readingState = 'DEFAULT_VALUE';
65
+ result.key = actual;
66
+ actual = '';
67
+ continue;
68
+ }
69
+ if (readingState === 'KEY' && char === '|') {
70
+ readingState = 'NAMESPACE';
71
+ result.key = actual;
72
+ actual = '';
73
+ continue;
74
+ }
75
+ if (readingState === 'NAMESPACE' && char === '|') {
76
+ readingState = 'NAMESPACE';
77
+ addNamespace(actual);
78
+ actual = '';
79
+ continue;
80
+ }
81
+ if (readingState === 'NAMESPACE' && char === ',') {
82
+ readingState = 'DEFAULT_VALUE';
83
+ addNamespace(actual);
84
+ actual = '';
85
+ continue;
86
+ }
87
+ if (readingState === 'KEY' && char === ':') {
88
+ readingState = 'PARAM_NAME';
89
+ result.key = actual;
90
+ actual = '';
91
+ continue;
92
+ }
93
+
94
+ if (readingState === 'DEFAULT_VALUE' && char === ':') {
95
+ readingState = 'PARAM_NAME';
96
+ result.defaultValue = actual;
97
+ actual = '';
98
+ continue;
99
+ }
100
+
101
+ if (readingState === 'PARAM_NAME' && char === ':') {
102
+ readingState = 'PARAM_VALUE';
103
+ paramName = actual;
104
+ actual = '';
105
+ continue;
106
+ }
107
+
108
+ if (readingState === 'PARAM_VALUE' && char === ',') {
109
+ readingState = 'PARAM_NAME';
110
+ result.params![paramName] = actual;
111
+ actual = '';
112
+ continue;
113
+ }
114
+ actual += char;
115
+ }
116
+
117
+ if (readingState === 'KEY') {
118
+ result.key = actual;
119
+ }
120
+
121
+ if (readingState === 'DEFAULT_VALUE') {
122
+ result.defaultValue = actual;
123
+ }
124
+
125
+ if (readingState === 'PARAM_VALUE') {
126
+ result.params![paramName] = actual;
127
+ }
128
+
129
+ if (readingState === 'NAMESPACE') {
130
+ addNamespace(actual);
131
+ }
132
+
133
+ return result;
134
+ }
135
+
136
+ const unwrap: WrapperUnwrapFunction = (text: string) => {
137
+ const matchRegexp = new RegExp(getRawUnWrapRegex(), 'gs');
138
+
139
+ const keysAndParams: KeyAndParams[] = [];
140
+
141
+ let matched = false;
142
+
143
+ let match;
144
+ let start = 0;
145
+ let result = '';
146
+ while ((match = matchRegexp.exec(text)) !== null) {
147
+ let pre = match[1] as string;
148
+ const [fullMatch, _, wrapped, unwrapped] = match as unknown as [
149
+ string,
150
+ string,
151
+ string,
152
+ string
153
+ ];
154
+ const { index, input } = match;
155
+ result += input.substr(start, index - start);
156
+ start = index + fullMatch.length;
157
+ if (pre === '\\') {
158
+ if (!isCharEscaped(index, text)) {
159
+ result += wrapped;
160
+ continue;
161
+ }
162
+ pre = '';
163
+ }
164
+ const translated = getTranslatedWithMetadata(unwrapped);
165
+ keysAndParams.push({
166
+ key: translated.key,
167
+ params: translated.params,
168
+ defaultValue: translated.defaultValue,
169
+ });
170
+ matched = true;
171
+ result += pre + translated.translated;
172
+ }
173
+
174
+ result += text.substring(start);
175
+
176
+ if (matched) {
177
+ return { text: result, keys: keysAndParams };
178
+ }
179
+
180
+ return { text: text, keys: [] };
181
+ };
182
+
183
+ const wrap: WrapperWrapFunction = ({
184
+ key,
185
+ params,
186
+ defaultValue,
187
+ ns,
188
+ }): string => {
189
+ let paramString = Object.entries(params || {})
190
+ .map(
191
+ ([name, value]) =>
192
+ `${escapeParam(name)}:${escapeParam(value as string)}`
193
+ )
194
+ .join(',');
195
+ paramString = paramString.length ? `:${paramString}` : '';
196
+
197
+ const defaultString =
198
+ defaultValue !== undefined ? `,${escapeParam(defaultValue)}` : '';
199
+
200
+ const nsArray = typeof ns === 'string' ? [ns] : ns;
201
+
202
+ const namespaces = nsArray?.length
203
+ ? `|${nsArray.map((ns) => escapeParam(ns)).join('|')}`
204
+ : '';
205
+
206
+ return `${inputPrefix}${escapeParam(
207
+ key
208
+ )}${namespaces}${defaultString}${paramString}${inputSuffix}`;
209
+ };
210
+
211
+ function getTranslatedWithMetadata(text: string) {
212
+ const { key, params, defaultValue, ns } = parseUnwrapped(text);
213
+ const translated = translate({
214
+ key,
215
+ params,
216
+ defaultValue,
217
+ ns,
218
+ noWrap: true,
219
+ });
220
+ return { translated, key, params, defaultValue };
221
+ }
222
+
223
+ const escapeForRegExp = (string: string) => {
224
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
225
+ };
226
+
227
+ const escapeParam = (param: any) => {
228
+ if (typeof param === 'string') {
229
+ return param.replace(/[,:|\\]/gs, '\\$&');
230
+ }
231
+ if (typeof param === 'number' || typeof param === 'bigint') {
232
+ return param.toString();
233
+ }
234
+ // eslint-disable-next-line no-console
235
+ console.warn(
236
+ `Parameters of type "${typeof param}" are not supported in "text" wrapper mode.`
237
+ );
238
+ return param;
239
+ };
240
+
241
+ const getTextXPath = () => {
242
+ return `./descendant-or-self::text()[contains(., '${inputPrefix}') and contains(., '${inputSuffix}')]`;
243
+ };
244
+
245
+ const getAttributeXPath: WrapperAttributeXPathGetter = ({
246
+ tag,
247
+ attribute,
248
+ }) => {
249
+ return `descendant-or-self::${tag}/@${attribute}[contains(., '${inputPrefix}') and contains(., '${inputSuffix}')]`;
250
+ };
251
+
252
+ return Object.freeze({
253
+ wrap,
254
+ unwrap,
255
+ getTextXPath,
256
+ getAttributeXPath,
257
+ });
258
+ };
@@ -0,0 +1,56 @@
1
+ export function splitOnNonEscapedDelimiter(
2
+ string: string,
3
+ delimiter: string
4
+ ): string[] {
5
+ const result = [];
6
+ let actual = '';
7
+ let escaped = false;
8
+ for (let i = 0; i < string.length; i++) {
9
+ const char = string[i];
10
+ if (char === '\\' && !escaped) {
11
+ escaped = true;
12
+ continue;
13
+ }
14
+ if (escaped) {
15
+ escaped = false;
16
+ actual += char;
17
+ continue;
18
+ }
19
+ if (char === delimiter) {
20
+ result.push(actual);
21
+ actual = '';
22
+ continue;
23
+ }
24
+ actual += char;
25
+ }
26
+ result.push(actual);
27
+ return result;
28
+ }
29
+
30
+ export function isCharEscaped(position: number, fullString: string) {
31
+ let escapeCharsCount = 0;
32
+ while (position > -1 && fullString[position - 1] === '\\') {
33
+ escapeCharsCount++;
34
+ position--;
35
+ }
36
+ return escapeCharsCount % 2 == 1;
37
+ }
38
+
39
+ export function removeEscapes(string: string) {
40
+ let result = '';
41
+ let escaped = false;
42
+ for (let i = 0; i < string.length; i++) {
43
+ const char = string[i];
44
+ if (char === '\\' && !escaped) {
45
+ escaped = true;
46
+ continue;
47
+ }
48
+ if (escaped) {
49
+ escaped = false;
50
+ result += char;
51
+ continue;
52
+ }
53
+ result += char;
54
+ }
55
+ return result;
56
+ }
@@ -0,0 +1,14 @@
1
+ import { getProjectIdFromApiKey } from './decodeApiKey';
2
+
3
+ const PAK_KEY = 'tgpak_gfpxm4lin4zdazleoq4gm2rumfxgi2lfom2gw4dpguzxc';
4
+ const OLD_KEY = 'ryj4psai6vetel5b27ven6fajf';
5
+
6
+ describe('get projectId from api key', () => {
7
+ it('can decode from pak key', () => {
8
+ expect(getProjectIdFromApiKey(PAK_KEY)).toEqual(1);
9
+ });
10
+
11
+ it("won't fail on legacy code", () => {
12
+ expect(getProjectIdFromApiKey(OLD_KEY)).toBeUndefined();
13
+ });
14
+ });
@@ -0,0 +1,74 @@
1
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
2
+
3
+ function readChar(char: string) {
4
+ const idx = alphabet.indexOf(char);
5
+
6
+ if (idx === -1) {
7
+ throw new Error('Invalid character found: ' + char);
8
+ }
9
+
10
+ return idx;
11
+ }
12
+
13
+ function arrayBufferToString(buffer: any) {
14
+ const bufView = new Uint8Array(buffer);
15
+ const length = bufView.length;
16
+ let result = '';
17
+ let addition = Math.pow(2, 16) - 1;
18
+
19
+ for (let i = 0; i < length; i += addition) {
20
+ if (i + addition > length) {
21
+ addition = length - i;
22
+ }
23
+ result += String.fromCharCode.apply(
24
+ null,
25
+ // @ts-ignore
26
+ bufView.subarray(i, i + addition)
27
+ );
28
+ }
29
+
30
+ return result;
31
+ }
32
+
33
+ function base32Decode(input: string) {
34
+ input = input.toUpperCase();
35
+
36
+ const length = input.length;
37
+
38
+ let bits = 0;
39
+ let value = 0;
40
+
41
+ let index = 0;
42
+ const output = new Uint8Array(((length * 5) / 8) | 0);
43
+
44
+ for (let i = 0; i < length; i++) {
45
+ value = (value << 5) | readChar(input[i]);
46
+ bits += 5;
47
+
48
+ if (bits >= 8) {
49
+ output[index++] = (value >>> (bits - 8)) & 255;
50
+ bits -= 8;
51
+ }
52
+ }
53
+
54
+ return arrayBufferToString(output.buffer);
55
+ }
56
+
57
+ export function getProjectIdFromApiKey(
58
+ key: string | undefined
59
+ ): number | undefined {
60
+ if (!key) {
61
+ return undefined;
62
+ }
63
+ try {
64
+ const [prefix, rest] = key.split('_');
65
+ if (prefix === 'tgpak') {
66
+ const [projectId] = base32Decode(rest).split('_');
67
+ return Number(projectId);
68
+ }
69
+ } catch {
70
+ // eslint-disable-next-line no-console
71
+ console.warn("Tolgee: Api key can't be parsed");
72
+ }
73
+ return undefined;
74
+ }
@@ -0,0 +1,159 @@
1
+ import { sleep } from '../ui/tools/sleep';
2
+ import {
3
+ detectExtension,
4
+ Handshaker,
5
+ sendAndRecieve,
6
+ takeScreenshot,
7
+ } from './extension';
8
+
9
+ describe('communicates with extension correctly', () => {
10
+ let registerListener = jest.fn();
11
+ let postMessageMock = jest.fn();
12
+ beforeEach(() => {
13
+ registerListener = jest.fn();
14
+ window.addEventListener = jest.fn((_, listener) =>
15
+ registerListener(listener)
16
+ );
17
+ window.removeEventListener = jest.fn();
18
+
19
+ postMessageMock = jest.fn();
20
+ window.postMessage = postMessageMock;
21
+
22
+ // @ts-ignore
23
+ window.setTimeout = jest.fn((callback) =>
24
+ Promise.resolve().then(() => callback())
25
+ );
26
+ });
27
+ afterEach(() => {
28
+ jest.resetAllMocks();
29
+ });
30
+
31
+ it('sends correct message', async () => {
32
+ const { promise } = sendAndRecieve({
33
+ message: 'PING',
34
+ recievingMessage: ['PONG'],
35
+ data: 'testData',
36
+ });
37
+ registerListener.mock.calls[0][0]({
38
+ data: { type: 'PONG', data: 'resultData' },
39
+ });
40
+ const result = await promise;
41
+ expect(result).toEqual('resultData');
42
+ expect(postMessageMock.mock.calls[0][0]).toEqual({
43
+ type: 'PING',
44
+ data: 'testData',
45
+ });
46
+ });
47
+
48
+ it('repeats message correctly', async () => {
49
+ const { promise } = sendAndRecieve({
50
+ message: 'PING',
51
+ recievingMessage: ['PONG'],
52
+ data: 'testData',
53
+ attempts: 7,
54
+ });
55
+ const result = await promise.catch(() => 'error');
56
+ expect(postMessageMock).toBeCalledTimes(7);
57
+ expect(result).toEqual('error');
58
+ });
59
+
60
+ it('cancels repetition', async () => {
61
+ const { promise, cancel } = sendAndRecieve({
62
+ message: 'PAT',
63
+ recievingMessage: ['MAT'],
64
+ data: 'testData',
65
+ attempts: 7,
66
+ });
67
+ promise.catch(() => 'error');
68
+ cancel();
69
+ await sleep(10);
70
+ expect(postMessageMock).toBeCalledTimes(1);
71
+ });
72
+
73
+ it('ping detects if extension is present', async () => {
74
+ const promise = detectExtension();
75
+ expect(postMessageMock.mock.calls[0][0].type).toEqual('TOLGEE_PING');
76
+ registerListener.mock.calls[0][0]({
77
+ data: { type: 'TOLGEE_PONG', data: undefined },
78
+ });
79
+ const result = await promise;
80
+ expect(result).toEqual(true);
81
+ expect(registerListener).toBeCalled();
82
+ expect(window.removeEventListener).toBeCalled();
83
+ });
84
+
85
+ it('ping timeouts if extension is not present', async () => {
86
+ const result = await detectExtension();
87
+ expect(result).toEqual(false);
88
+ expect(postMessageMock).toBeCalledTimes(2);
89
+ });
90
+
91
+ it('take screenshot works', async () => {
92
+ const promise = takeScreenshot();
93
+ registerListener.mock.calls[0][0]({
94
+ data: { type: 'TOLGEE_SCREENSHOT_TAKEN', data: 'test' },
95
+ });
96
+
97
+ const result = await promise;
98
+ expect(result).toEqual('test');
99
+ });
100
+
101
+ it('take screenshot throws error', async () => {
102
+ const result = await takeScreenshot().catch(() => 'error');
103
+ expect(result).toEqual('error');
104
+ });
105
+
106
+ ['TOLGEE_PLUGIN_READY', 'TOLGEE_PLUGIN_UPDATED'].forEach((event) => {
107
+ it(`handshaker works with ${event}`, async () => {
108
+ const handshaker = Handshaker();
109
+ const promise = handshaker.update({
110
+ uiPresent: false,
111
+ mode: 'production',
112
+ config: {
113
+ apiUrl: 'test',
114
+ apiKey: 'test',
115
+ },
116
+ });
117
+ expect(registerListener).toBeCalled();
118
+
119
+ registerListener.mock.calls[0][0]({
120
+ data: { type: event, data: true },
121
+ });
122
+
123
+ const result = await promise;
124
+
125
+ expect(result).toEqual(true);
126
+ });
127
+ });
128
+
129
+ it('handshaker throws error if extension not present', async () => {
130
+ const handshaker = Handshaker();
131
+ const promise = handshaker.update({
132
+ uiPresent: false,
133
+ mode: 'production',
134
+ config: {
135
+ apiUrl: 'test',
136
+ apiKey: 'test',
137
+ },
138
+ });
139
+
140
+ const result = await promise.catch(() => 'error');
141
+ expect(result).toEqual('error');
142
+ });
143
+
144
+ it('handshaker will send only the last message', async () => {
145
+ const handshaker = Handshaker();
146
+ handshaker.update('first message' as any);
147
+ await handshaker.update('second message' as any).catch(() => 'error');
148
+ expect(
149
+ postMessageMock.mock.calls.filter(
150
+ (args) => args[0].data === 'first message'
151
+ )
152
+ ).toHaveLength(1);
153
+ expect(
154
+ postMessageMock.mock.calls.filter(
155
+ (args) => args[0].data === 'second message'
156
+ )
157
+ ).toHaveLength(4);
158
+ });
159
+ });