@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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Translations, TreeTranslationsData } from '../types';
|
|
2
|
+
import { Properties } from '../Properties';
|
|
3
|
+
import { CoreService } from './CoreService';
|
|
4
|
+
import { ApiHttpService } from './ApiHttpService';
|
|
5
|
+
import { ApiHttpError } from '../Errors/ApiHttpError';
|
|
6
|
+
import { EventService } from './EventService';
|
|
7
|
+
import { EventEmitterImpl } from './EventEmitter';
|
|
8
|
+
import {
|
|
9
|
+
ComplexEditKeyDto,
|
|
10
|
+
CreateKeyDto,
|
|
11
|
+
KeyWithDataModel,
|
|
12
|
+
KeyWithTranslationsModel,
|
|
13
|
+
SetTranslationsResponseModel,
|
|
14
|
+
SetTranslationsWithKeyDto,
|
|
15
|
+
TranslationData,
|
|
16
|
+
} from '../types/DTOs';
|
|
17
|
+
|
|
18
|
+
interface TranslationInterface {
|
|
19
|
+
text?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Key {
|
|
23
|
+
id?: number;
|
|
24
|
+
name: string;
|
|
25
|
+
translations: Record<string, TranslationInterface>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class TranslationService {
|
|
29
|
+
private translationsCache: Map<string, Translations> = new Map<
|
|
30
|
+
string,
|
|
31
|
+
Translations
|
|
32
|
+
>();
|
|
33
|
+
private fetchPromises: { [key: string]: Promise<any> } = {};
|
|
34
|
+
|
|
35
|
+
// we need to distinguish which languages are in cache initially
|
|
36
|
+
// because we need to refetch them in dev mode
|
|
37
|
+
private fetchedDev: { [key: string]: boolean } = {};
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private properties: Properties,
|
|
41
|
+
private coreService: CoreService,
|
|
42
|
+
private apiHttpService: ApiHttpService,
|
|
43
|
+
private eventService: EventService
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
private static translationByValue(message: string, defaultValue?: string) {
|
|
47
|
+
if (message) {
|
|
48
|
+
return message;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (defaultValue) {
|
|
52
|
+
return defaultValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initStatic() {
|
|
59
|
+
if (typeof this.properties.config?.staticData === 'object') {
|
|
60
|
+
Object.entries(this.properties.config.staticData).forEach(
|
|
61
|
+
([language, data]) => {
|
|
62
|
+
//if not provider or promise then it is raw data
|
|
63
|
+
if (typeof data !== 'function') {
|
|
64
|
+
this.setLanguageData(language, data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getCachedTranslations() {
|
|
72
|
+
return this.translationsCache;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
updateTranslationInCache = async (data: Key) => {
|
|
76
|
+
const result: Record<string, string> = {};
|
|
77
|
+
Object.entries(data.translations).forEach(([lang, translation]) => {
|
|
78
|
+
const cachedData = this.translationsCache.get(lang);
|
|
79
|
+
if (cachedData) {
|
|
80
|
+
cachedData[data.name] = translation.text;
|
|
81
|
+
}
|
|
82
|
+
result[lang] = translation.text;
|
|
83
|
+
});
|
|
84
|
+
await (
|
|
85
|
+
this.eventService.TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
|
|
86
|
+
).emit(new TranslationData(data.name, result, data.id));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async loadTranslations(lang: string = this.properties.currentLanguage) {
|
|
90
|
+
if (this.isFetchNeeded(lang)) {
|
|
91
|
+
if (!(this.fetchPromises[lang] instanceof Promise)) {
|
|
92
|
+
this.fetchPromises[lang] = this.fetchTranslations(lang);
|
|
93
|
+
}
|
|
94
|
+
await this.fetchPromises[lang];
|
|
95
|
+
(this.eventService.LANGUAGE_LOADED as EventEmitterImpl<string>).emit(
|
|
96
|
+
lang
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
this.fetchPromises[lang] = undefined;
|
|
100
|
+
return this.translationsCache.get(lang);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getTranslation(
|
|
104
|
+
key: string,
|
|
105
|
+
lang: string = this.properties.currentLanguage,
|
|
106
|
+
defaultValue?: string
|
|
107
|
+
): Promise<string> {
|
|
108
|
+
if (this.isFetchNeeded(lang)) {
|
|
109
|
+
await this.loadTranslations(lang);
|
|
110
|
+
}
|
|
111
|
+
let message = this.getFromCache(key, lang);
|
|
112
|
+
|
|
113
|
+
if (!message) {
|
|
114
|
+
// try to get translation from fallback language
|
|
115
|
+
const fallbackLang = this.properties.config.fallbackLanguage;
|
|
116
|
+
if (this.isFetchNeeded(fallbackLang)) {
|
|
117
|
+
await this.loadTranslations(this.properties.config.fallbackLanguage);
|
|
118
|
+
}
|
|
119
|
+
message = this.getFromCache(key, this.properties.config.fallbackLanguage);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return TranslationService.translationByValue(message, defaultValue);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async updateKeyComplex(
|
|
126
|
+
id: number,
|
|
127
|
+
data: ComplexEditKeyDto
|
|
128
|
+
): Promise<KeyWithDataModel> {
|
|
129
|
+
this.coreService.checkScope('translations.edit');
|
|
130
|
+
const result = (await this.apiHttpService.postJson(
|
|
131
|
+
`v2/projects/keys/${id}/complex-update`,
|
|
132
|
+
{
|
|
133
|
+
...data,
|
|
134
|
+
screenshotUploadedImageIds: data.screenshotUploadedImageIds?.length
|
|
135
|
+
? data.screenshotUploadedImageIds
|
|
136
|
+
: undefined,
|
|
137
|
+
screenshotIdsToDelete: data.screenshotIdsToDelete?.length
|
|
138
|
+
? data.screenshotIdsToDelete
|
|
139
|
+
: undefined,
|
|
140
|
+
},
|
|
141
|
+
{ method: 'put' }
|
|
142
|
+
)) as KeyWithDataModel;
|
|
143
|
+
|
|
144
|
+
await this.updateTranslationInCache(result);
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async createKey(data: CreateKeyDto): Promise<KeyWithDataModel> {
|
|
150
|
+
this.coreService.checkScope('keys.edit');
|
|
151
|
+
const result = (await this.apiHttpService.postJson(
|
|
152
|
+
`v2/projects/keys/create`,
|
|
153
|
+
{
|
|
154
|
+
...data,
|
|
155
|
+
screenshotUploadedImageIds: data.screenshotUploadedImageIds?.length
|
|
156
|
+
? data.screenshotUploadedImageIds
|
|
157
|
+
: undefined,
|
|
158
|
+
}
|
|
159
|
+
)) as KeyWithDataModel;
|
|
160
|
+
|
|
161
|
+
await this.updateTranslationInCache(result);
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async setTranslations(
|
|
167
|
+
translationData: SetTranslationsWithKeyDto
|
|
168
|
+
): Promise<SetTranslationsResponseModel> {
|
|
169
|
+
this.coreService.checkScope('translations.edit');
|
|
170
|
+
const result = (await this.apiHttpService.postJson(
|
|
171
|
+
'v2/projects/translations',
|
|
172
|
+
translationData
|
|
173
|
+
)) as SetTranslationsResponseModel;
|
|
174
|
+
|
|
175
|
+
await this.updateTranslationInCache({
|
|
176
|
+
id: result.keyId,
|
|
177
|
+
name: result.keyName,
|
|
178
|
+
translations: result.translations,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Change translations of some keys to some value temporarily.
|
|
186
|
+
* For screenshot taking with provided values, before actually saving
|
|
187
|
+
* the values
|
|
188
|
+
*
|
|
189
|
+
* @return Returns callback changing affected translations back
|
|
190
|
+
*/
|
|
191
|
+
async changeTranslations({
|
|
192
|
+
key,
|
|
193
|
+
translations,
|
|
194
|
+
}: TranslationData): Promise<() => void> {
|
|
195
|
+
const old: Record<string, string> = {};
|
|
196
|
+
|
|
197
|
+
Object.entries(translations).forEach(([language, value]) => {
|
|
198
|
+
const data = this.translationsCache.get(language);
|
|
199
|
+
if (data) {
|
|
200
|
+
old[language] = data[key];
|
|
201
|
+
data[key] = value;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await (
|
|
206
|
+
this.eventService.TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
|
|
207
|
+
).emit({
|
|
208
|
+
key,
|
|
209
|
+
translations,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// callback to revert the operation
|
|
213
|
+
return async () => {
|
|
214
|
+
Object.entries(old).forEach(([language, value]) => {
|
|
215
|
+
const data = this.translationsCache.get(language);
|
|
216
|
+
if (data) {
|
|
217
|
+
data[key] = value;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
await (
|
|
221
|
+
this.eventService
|
|
222
|
+
.TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
|
|
223
|
+
).emit({
|
|
224
|
+
key,
|
|
225
|
+
translations: old,
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getFromCacheOrFallback(
|
|
231
|
+
key: string,
|
|
232
|
+
lang: string = this.properties.currentLanguage,
|
|
233
|
+
defaultValue?: string
|
|
234
|
+
): string {
|
|
235
|
+
const fallbackLang = this.properties.config.fallbackLanguage;
|
|
236
|
+
const message =
|
|
237
|
+
this.getFromCache(key, lang) || this.getFromCache(key, fallbackLang);
|
|
238
|
+
|
|
239
|
+
if (!message && (!this.isLoaded(lang) || !this.isLoaded(fallbackLang))) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return TranslationService.translationByValue(message, defaultValue);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getTranslationsOfKey = async (
|
|
246
|
+
key: string,
|
|
247
|
+
languages: Set<string> = new Set([this.properties.currentLanguage])
|
|
248
|
+
) => {
|
|
249
|
+
this.coreService.checkScope('translations.view');
|
|
250
|
+
try {
|
|
251
|
+
const languagesArray = [...languages];
|
|
252
|
+
const languagesQuery = languagesArray
|
|
253
|
+
.map((l) => `languages=${l}`)
|
|
254
|
+
.join('&');
|
|
255
|
+
const data = await this.apiHttpService.fetchJson(
|
|
256
|
+
`v2/projects/translations?${languagesQuery}&filterKeyName=${encodeURIComponent(
|
|
257
|
+
key
|
|
258
|
+
)}`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const translationData = languagesArray.reduce(
|
|
262
|
+
(acc, curr) => ({ ...acc, [curr]: '' }),
|
|
263
|
+
{}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const firstItem = data._embedded?.keys?.[0] as
|
|
267
|
+
| KeyWithTranslationsModel
|
|
268
|
+
| undefined;
|
|
269
|
+
|
|
270
|
+
if (firstItem?.translations) {
|
|
271
|
+
Object.entries(firstItem.translations).forEach(
|
|
272
|
+
([language, translation]) =>
|
|
273
|
+
(translationData[language] = (translation as any).text)
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const langs = data.selectedLanguages?.map((l) => l.tag) as string[];
|
|
278
|
+
|
|
279
|
+
return [firstItem, langs] as const;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (
|
|
282
|
+
e instanceof ApiHttpError &&
|
|
283
|
+
e.response.status === 404 &&
|
|
284
|
+
e.code === 'language_not_found'
|
|
285
|
+
) {
|
|
286
|
+
// only possible reason for this error is, that languages definition
|
|
287
|
+
// is changed, but the old value is stored in preferred languages
|
|
288
|
+
this.properties.preferredLanguages =
|
|
289
|
+
await this.coreService.getLanguages();
|
|
290
|
+
// eslint-disable-next-line no-console
|
|
291
|
+
console.error('Requested language not found, refreshing the page!');
|
|
292
|
+
location.reload();
|
|
293
|
+
} else {
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
private isFetchNeeded(lang: string) {
|
|
300
|
+
const isDevMode = this.properties.mode === 'development';
|
|
301
|
+
const dataPresent = this.translationsCache.get(lang) !== undefined;
|
|
302
|
+
const devFetched = Boolean(this.fetchedDev[lang]);
|
|
303
|
+
return (isDevMode && !devFetched) || !dataPresent;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private isLoaded(lang: string) {
|
|
307
|
+
return this.translationsCache.get(lang) !== undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async fetchTranslations(lang: string) {
|
|
311
|
+
const isDevMode = this.properties.mode === 'development';
|
|
312
|
+
if (isDevMode) {
|
|
313
|
+
return await this.fetchTranslationsDevelopment(lang);
|
|
314
|
+
} else {
|
|
315
|
+
return await this.fetchTranslationsProduction(lang);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async fetchTranslationsProduction(language: string) {
|
|
320
|
+
const langStaticData = this.properties.config?.staticData?.[language];
|
|
321
|
+
|
|
322
|
+
if (typeof langStaticData === 'function') {
|
|
323
|
+
const data = await langStaticData();
|
|
324
|
+
this.setLanguageData(language, data);
|
|
325
|
+
return;
|
|
326
|
+
} else if (langStaticData !== undefined) {
|
|
327
|
+
this.setLanguageData(language, langStaticData);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const url = `${
|
|
332
|
+
this.properties.config.filesUrlPrefix || '/'
|
|
333
|
+
}${language}.json`;
|
|
334
|
+
try {
|
|
335
|
+
const result = await fetch(url);
|
|
336
|
+
if (result.status >= 400) {
|
|
337
|
+
//on error set language data as empty object to not break the flow
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.error(
|
|
340
|
+
'Server responded with error status while loading localization data.'
|
|
341
|
+
);
|
|
342
|
+
this.setLanguageData(language, {});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const data = await result.json();
|
|
347
|
+
this.setLanguageData(language, data);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
// eslint-disable-next-line no-console
|
|
350
|
+
console.error(`Error parsing json retrieved from ${url}.`);
|
|
351
|
+
this.setEmptyLanguageData(language);
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// eslint-disable-next-line no-console
|
|
355
|
+
console.error(`Error fetching localization data from ${url}.`);
|
|
356
|
+
this.setEmptyLanguageData(language);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async fetchTranslationsDevelopment(language: string) {
|
|
361
|
+
await this.coreService.loadApiKeyDetails();
|
|
362
|
+
this.coreService.checkScope('translations.view');
|
|
363
|
+
try {
|
|
364
|
+
const data = await this.apiHttpService.fetchJson(
|
|
365
|
+
`v2/projects/translations/${language}`
|
|
366
|
+
);
|
|
367
|
+
this.fetchedDev[language] = true;
|
|
368
|
+
this.setLanguageData(language, data[language] || {});
|
|
369
|
+
} catch (e) {
|
|
370
|
+
// eslint-disable-next-line no-console
|
|
371
|
+
console.error('Error while fetching localization data from API.', e);
|
|
372
|
+
this.setEmptyLanguageData(language);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private setEmptyLanguageData(language: string) {
|
|
378
|
+
this.translationsCache.set(language, {});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private setLanguageData(language: string, data: TreeTranslationsData) {
|
|
382
|
+
// recursively walk the tree and make it flat, when tree data are provided
|
|
383
|
+
const makeFlat = (data: TreeTranslationsData): Record<string, string> => {
|
|
384
|
+
const result: Record<string, string> = {};
|
|
385
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
386
|
+
// ignore falsy values
|
|
387
|
+
if (!value) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (typeof value === 'object') {
|
|
391
|
+
Object.entries(makeFlat(value)).forEach(([flatKey, flatValue]) => {
|
|
392
|
+
result[key + '.' + flatKey] = flatValue;
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
result[key] = value as string;
|
|
397
|
+
});
|
|
398
|
+
return result;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
this.translationsCache.set(language, makeFlat(data));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private getFromCache(
|
|
405
|
+
key: string,
|
|
406
|
+
lang: string = this.properties.currentLanguage
|
|
407
|
+
): string {
|
|
408
|
+
const root: string | Translations = this.translationsCache.get(lang);
|
|
409
|
+
|
|
410
|
+
//if lang is not downloaded or does not exist at all
|
|
411
|
+
if (root === undefined) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return root[key] as string;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Module from '../CoreService';
|
|
2
|
+
import classMock from '@testFixtures/classMock';
|
|
3
|
+
import { Scope } from '../../types';
|
|
4
|
+
|
|
5
|
+
const moduleMock = jest.genMockFromModule('../CoreService');
|
|
6
|
+
|
|
7
|
+
export const CoreService = classMock<Module.CoreService>(
|
|
8
|
+
() => ({
|
|
9
|
+
getApiKeyDetails: jest.fn(async () => {
|
|
10
|
+
return {
|
|
11
|
+
scopes: ['translations.edit', 'keys.edit'] as Scope[],
|
|
12
|
+
projectId: 0,
|
|
13
|
+
} as Module.ApiKeyWithLanguagesModel;
|
|
14
|
+
}),
|
|
15
|
+
}),
|
|
16
|
+
(moduleMock as typeof Module).CoreService
|
|
17
|
+
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
jest.dontMock('./Messages');
|
|
2
|
+
jest.dontMock('../services/DependencyService');
|
|
3
|
+
|
|
4
|
+
import { Messages } from './Messages';
|
|
5
|
+
import { DependencyService } from '../services/DependencyService';
|
|
6
|
+
|
|
7
|
+
describe('Messages', () => {
|
|
8
|
+
let messages: Messages;
|
|
9
|
+
let windowAddEventListenerSpy;
|
|
10
|
+
let windowRemoveEventListenerSpy;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
messages = new DependencyService().messages;
|
|
14
|
+
windowAddEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
|
15
|
+
windowRemoveEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
|
16
|
+
messages.startListening();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Can be created', () => {
|
|
24
|
+
expect(messages).toBeInstanceOf(Messages);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('starListening method', () => {
|
|
28
|
+
test('adds event listener to window', () => {
|
|
29
|
+
expect(windowAddEventListenerSpy).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('listening', () => {
|
|
34
|
+
let resolve;
|
|
35
|
+
const promise = new Promise((r) => (resolve = r));
|
|
36
|
+
const callbackMock = jest.fn(() => {
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
const data = {};
|
|
40
|
+
let removeListener;
|
|
41
|
+
const sendMessage = () =>
|
|
42
|
+
window.dispatchEvent(
|
|
43
|
+
new MessageEvent('message', {
|
|
44
|
+
data: { type: 'TEST_EVENT', data },
|
|
45
|
+
source: window,
|
|
46
|
+
origin: window.origin,
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
removeListener = messages.listen('TEST_EVENT', callbackMock);
|
|
52
|
+
sendMessage();
|
|
53
|
+
await promise;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('listens to event', async () => {
|
|
57
|
+
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(callbackMock).toHaveBeenCalledWith(data);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('returns callback to remove listener', () => {
|
|
62
|
+
expect((messages as any).listeners).toHaveLength(1);
|
|
63
|
+
removeListener();
|
|
64
|
+
expect((messages as any).listeners).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('stopListening method', () => {
|
|
69
|
+
test('stopsListening', () => {
|
|
70
|
+
messages.stopListening();
|
|
71
|
+
expect(windowRemoveEventListenerSpy).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith(
|
|
73
|
+
windowAddEventListenerSpy.mock.calls[0][0],
|
|
74
|
+
windowAddEventListenerSpy.mock.calls[0][1],
|
|
75
|
+
windowAddEventListenerSpy.mock.calls[0][2]
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type Listener = {
|
|
2
|
+
type: string;
|
|
3
|
+
callback: (data) => void;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type Message = {
|
|
7
|
+
data: any;
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TolgeeEvent = {
|
|
12
|
+
data: Message;
|
|
13
|
+
} & MessageEvent;
|
|
14
|
+
|
|
15
|
+
export class Messages {
|
|
16
|
+
private listeners: Listener[] = [];
|
|
17
|
+
private _stopListening: () => void;
|
|
18
|
+
|
|
19
|
+
readonly startListening = () => {
|
|
20
|
+
const receiveMessage = (event: TolgeeEvent) => {
|
|
21
|
+
if (event.source != window) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.listeners.forEach((listener) => {
|
|
25
|
+
if (listener.type == event.data.type) {
|
|
26
|
+
listener.callback(event.data.data);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
window.addEventListener('message', receiveMessage, false);
|
|
32
|
+
|
|
33
|
+
typeof this._stopListening === 'function' && this._stopListening();
|
|
34
|
+
this._stopListening = () => {
|
|
35
|
+
window.removeEventListener('message', receiveMessage, false);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
public stopListening() {
|
|
40
|
+
this._stopListening();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readonly listen = (type: string, callback: (data) => void) => {
|
|
44
|
+
const listenerInfo = { type, callback };
|
|
45
|
+
this.listeners.push(listenerInfo);
|
|
46
|
+
// return callback to remove the listener
|
|
47
|
+
return () => {
|
|
48
|
+
this.listeners.splice(this.listeners.indexOf(listenerInfo), 1);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
readonly send = (type: string, data?: any) => {
|
|
53
|
+
try {
|
|
54
|
+
window.postMessage({ type, data }, window.origin);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn('Cannot send message.', e);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
jest.dontMock('./PluginManager');
|
|
2
|
+
jest.dontMock('../services/DependencyService');
|
|
3
|
+
|
|
4
|
+
import { ElementWithMeta } from '../types';
|
|
5
|
+
import { ElementRegistrar } from '../services/ElementRegistrar';
|
|
6
|
+
import { getMockedInstance } from '@testFixtures/mocked';
|
|
7
|
+
import { PluginManager } from './PluginManager';
|
|
8
|
+
import { DependencyService } from '../services/DependencyService';
|
|
9
|
+
import { TranslationService } from '../services/TranslationService';
|
|
10
|
+
import { Messages } from './Messages';
|
|
11
|
+
|
|
12
|
+
describe('PluginManager', () => {
|
|
13
|
+
let pluginManager: PluginManager;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
pluginManager = new DependencyService().pluginManager;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Can be created', () => {
|
|
24
|
+
expect(pluginManager).toBeInstanceOf(PluginManager);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('take screenshot methods', () => {
|
|
28
|
+
const data = {
|
|
29
|
+
key: 'test_key',
|
|
30
|
+
translations: { en: 'English!' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let resolve;
|
|
34
|
+
const highlightMock = jest.fn();
|
|
35
|
+
const unHighlightMock = jest.fn();
|
|
36
|
+
let listenCallback;
|
|
37
|
+
const cancelMock = jest.fn();
|
|
38
|
+
const revertMock = jest.fn();
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
getMockedInstance(TranslationService).changeTranslations = jest.fn(
|
|
42
|
+
() =>
|
|
43
|
+
new Promise((r) => {
|
|
44
|
+
resolve = r;
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
getMockedInstance(ElementRegistrar).findAllByKey = jest.fn(() => {
|
|
49
|
+
const element = document.createElement(
|
|
50
|
+
'span'
|
|
51
|
+
) as any as ElementWithMeta;
|
|
52
|
+
element._tolgee = {
|
|
53
|
+
highlight: highlightMock,
|
|
54
|
+
unhighlight: unHighlightMock,
|
|
55
|
+
} as any;
|
|
56
|
+
|
|
57
|
+
return [element];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
(getMockedInstance(Messages) as any).send = jest.fn();
|
|
61
|
+
|
|
62
|
+
(getMockedInstance(Messages) as any).listen = jest.fn((_, callback) => {
|
|
63
|
+
listenCallback = callback;
|
|
64
|
+
return cancelMock;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pluginManager.takeScreenshot(data);
|
|
68
|
+
resolve(revertMock);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('calls change translation', () => {
|
|
72
|
+
expect(
|
|
73
|
+
getMockedInstance(TranslationService).changeTranslations
|
|
74
|
+
).toHaveBeenCalledWith(data);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('sends the message to take screenshots', () => {
|
|
78
|
+
expect((getMockedInstance(Messages) as any).send).toHaveBeenCalledTimes(
|
|
79
|
+
1
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('highlights all', () => {
|
|
84
|
+
expect(
|
|
85
|
+
getMockedInstance(ElementRegistrar).findAllByKey
|
|
86
|
+
).toHaveBeenCalledWith(data.key);
|
|
87
|
+
expect(
|
|
88
|
+
getMockedInstance(ElementRegistrar).findAllByKey
|
|
89
|
+
).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(highlightMock).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('unhighlights all after', () => {
|
|
94
|
+
listenCallback();
|
|
95
|
+
expect(unHighlightMock).toHaveBeenCalledTimes(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('reverts the translation change', () => {
|
|
99
|
+
listenCallback();
|
|
100
|
+
expect(revertMock).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('cancels the listening', () => {
|
|
104
|
+
listenCallback();
|
|
105
|
+
expect(cancelMock).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|