@tolgee/core 5.0.0-rc.af99eec.0 → 5.0.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 (96) hide show
  1. package/README.md +174 -0
  2. package/README.njk.md +61 -0
  3. package/dist/tolgee.cjs.js +733 -357
  4. package/dist/tolgee.cjs.js.map +1 -1
  5. package/dist/tolgee.cjs.min.js +1 -1
  6. package/dist/tolgee.cjs.min.js.map +1 -1
  7. package/dist/{tolgee.esm.mjs → tolgee.esm.js} +732 -352
  8. package/dist/tolgee.esm.js.map +1 -0
  9. package/dist/tolgee.esm.min.mjs +1 -1
  10. package/dist/tolgee.esm.min.mjs.map +1 -1
  11. package/dist/tolgee.umd.js +733 -357
  12. package/dist/tolgee.umd.js.map +1 -1
  13. package/dist/tolgee.umd.min.js +1 -1
  14. package/dist/tolgee.umd.min.js.map +1 -1
  15. package/lib/Controller/Cache/Cache.d.ts +10 -9
  16. package/lib/Controller/Controller.d.ts +104 -45
  17. package/lib/Controller/Events/EventEmitter.d.ts +6 -0
  18. package/lib/Controller/Events/EventEmitterSelective.d.ts +7 -0
  19. package/lib/Controller/Events/Events.d.ts +14 -0
  20. package/lib/Controller/Plugins/Plugins.d.ts +12 -25
  21. package/lib/Controller/State/State.d.ts +27 -9
  22. package/lib/Controller/State/initState.d.ts +46 -15
  23. package/lib/Controller/State/observerOptions.d.ts +41 -0
  24. package/lib/Controller/ValueObserver.d.ts +5 -5
  25. package/lib/FormatSimple/FormatError.d.ts +7 -0
  26. package/lib/FormatSimple/FormatSimple.d.ts +2 -0
  27. package/lib/FormatSimple/formatParser.d.ts +1 -0
  28. package/lib/FormatSimple/formatter.d.ts +2 -0
  29. package/lib/TolgeeCore.d.ts +204 -0
  30. package/lib/TranslateParams.d.ts +1 -1
  31. package/lib/helpers.d.ts +8 -0
  32. package/lib/index.d.ts +4 -4
  33. package/lib/types/cache.d.ts +25 -0
  34. package/lib/types/events.d.ts +66 -0
  35. package/lib/types/general.d.ts +34 -0
  36. package/lib/types/index.d.ts +7 -0
  37. package/lib/types/plugin.d.ts +127 -0
  38. package/package.json +5 -4
  39. package/src/Controller/Cache/Cache.ts +39 -34
  40. package/src/Controller/Cache/helpers.ts +6 -6
  41. package/src/Controller/Controller.ts +81 -53
  42. package/src/Controller/Events/EventEmitter.ts +34 -0
  43. package/src/Controller/Events/EventEmitterSelective.test.ts +110 -0
  44. package/src/Controller/Events/EventEmitterSelective.ts +132 -0
  45. package/src/Controller/Events/Events.ts +69 -0
  46. package/src/Controller/Plugins/Plugins.ts +182 -133
  47. package/src/Controller/State/State.ts +43 -26
  48. package/src/Controller/State/initState.ts +97 -25
  49. package/src/Controller/State/observerOptions.ts +66 -0
  50. package/src/Controller/ValueObserver.ts +5 -2
  51. package/src/FormatSimple/FormatError.ts +26 -0
  52. package/src/FormatSimple/FormatSimple.ts +13 -0
  53. package/src/FormatSimple/formatParser.ts +133 -0
  54. package/src/FormatSimple/formatter.test.ts +190 -0
  55. package/src/FormatSimple/formatter.ts +19 -0
  56. package/src/TolgeeCore.ts +267 -0
  57. package/src/TranslateParams.test.ts +9 -12
  58. package/src/TranslateParams.ts +6 -5
  59. package/src/__test/backend.test.ts +6 -6
  60. package/src/__test/cache.test.ts +190 -0
  61. package/src/__test/client.test.ts +2 -2
  62. package/src/__test/events.test.ts +32 -7
  63. package/src/__test/format.simple.test.ts +14 -0
  64. package/src/__test/formatError.test.ts +61 -0
  65. package/src/__test/initialization.test.ts +15 -3
  66. package/src/__test/languageDetection.test.ts +14 -8
  67. package/src/__test/languageStorage.test.ts +10 -11
  68. package/src/__test/languages.test.ts +30 -6
  69. package/src/__test/loading.test.ts +2 -2
  70. package/src/__test/{namespacesFallback.test.ts → namespaces.fallback.test.ts} +10 -8
  71. package/src/__test/namespaces.test.ts +30 -7
  72. package/src/__test/options.test.ts +64 -0
  73. package/src/__test/plugins.test.ts +29 -18
  74. package/src/helpers.ts +53 -0
  75. package/src/index.ts +4 -10
  76. package/src/types/cache.ts +37 -0
  77. package/src/types/events.ts +85 -0
  78. package/src/types/general.ts +50 -0
  79. package/src/types/index.ts +19 -0
  80. package/src/types/plugin.ts +181 -0
  81. package/dist/tolgee.esm.mjs.map +0 -1
  82. package/lib/Controller/State/helpers.d.ts +0 -6
  83. package/lib/Events/EventEmitter.d.ts +0 -6
  84. package/lib/Events/EventEmitterSelective.d.ts +0 -15
  85. package/lib/Events/Events.d.ts +0 -50
  86. package/lib/Tolgee.d.ts +0 -2
  87. package/lib/constants.d.ts +0 -5
  88. package/lib/types.d.ts +0 -274
  89. package/src/Controller/State/helpers.ts +0 -41
  90. package/src/Events/EventEmitter.ts +0 -27
  91. package/src/Events/EventEmitterSelective.test.ts +0 -108
  92. package/src/Events/EventEmitterSelective.ts +0 -160
  93. package/src/Events/Events.ts +0 -66
  94. package/src/Tolgee.ts +0 -77
  95. package/src/constants.ts +0 -7
  96. package/src/types.ts +0 -380
@@ -4,25 +4,18 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tolgee/core"] = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- const EventEmitter = () => {
8
- let handlers = [];
9
- const listen = (handler) => {
10
- const handlerWrapper = (e) => {
11
- handler(e);
12
- };
13
- handlers.push(handlerWrapper);
14
- return {
15
- unsubscribe: () => {
16
- handlers = handlers.filter((i) => handlerWrapper !== i);
17
- },
18
- };
19
- };
20
- const emit = (data) => {
21
- handlers.forEach((handler) => handler({ value: data }));
22
- };
23
- return Object.freeze({ listen, emit });
7
+ function isPromise(value) {
8
+ return Boolean(value && typeof value.then === 'function');
9
+ }
10
+ const valueOrPromise = (value, callback) => {
11
+ if (isPromise(value)) {
12
+ return Promise.resolve(value).then(callback);
13
+ }
14
+ else {
15
+ return callback(value);
16
+ }
24
17
  };
25
-
18
+ const missingOptionError = (option) => `Tolgee: You need to specify '${option}' option`;
26
19
  function isObject(item) {
27
20
  return typeof item === 'object' && !Array.isArray(item) && item !== null;
28
21
  }
@@ -49,22 +42,40 @@
49
42
  function unique(arr) {
50
43
  return Array.from(new Set(arr));
51
44
  }
52
-
53
- function incrementInMap(map, value) {
54
- const currNum = map.get(value) || 0;
55
- map.set(value, currNum + 1);
45
+ function sanitizeUrl(url) {
46
+ return url ? url.replace(/\/+$/, '') : url;
56
47
  }
57
- function decrementInMap(map, value) {
58
- let currNum = map.get(value) || 1;
59
- currNum -= 1;
60
- if (currNum <= 0) {
61
- map.delete(value);
48
+ function getErrorMessage(error) {
49
+ if (typeof error === 'string') {
50
+ return error;
62
51
  }
63
- else {
64
- map.set(value, currNum);
52
+ else if (typeof (error === null || error === void 0 ? void 0 : error.message) === 'string') {
53
+ return error.message;
65
54
  }
66
55
  }
67
- const EventEmitterSelective = () => {
56
+
57
+ const EventEmitter = (isActive) => {
58
+ let handlers = [];
59
+ const listen = (handler) => {
60
+ const handlerWrapper = (e) => {
61
+ handler(e);
62
+ };
63
+ handlers.push(handlerWrapper);
64
+ return {
65
+ unsubscribe: () => {
66
+ handlers = handlers.filter((i) => handlerWrapper !== i);
67
+ },
68
+ };
69
+ };
70
+ const emit = (data) => {
71
+ if (isActive()) {
72
+ handlers.forEach((handler) => handler({ value: data }));
73
+ }
74
+ };
75
+ return Object.freeze({ listen, emit });
76
+ };
77
+
78
+ const EventEmitterSelective = (isActive, getFallbackNs, getDefaultNs) => {
68
79
  const listeners = new Set();
69
80
  const partialListeners = new Set();
70
81
  const listen = (handler) => {
@@ -81,8 +92,7 @@
81
92
  fn: (e) => {
82
93
  handler(e);
83
94
  },
84
- keys: new Map(),
85
- namespaces: new Map(),
95
+ namespaces: new Set(),
86
96
  };
87
97
  partialListeners.add(handlerWrapper);
88
98
  const result = {
@@ -90,105 +100,85 @@
90
100
  partialListeners.delete(handlerWrapper);
91
101
  },
92
102
  subscribeNs: (ns) => {
93
- getFallbackArray(ns).forEach((val) => incrementInMap(handlerWrapper.namespaces, val));
94
- return result;
95
- },
96
- unsubscribeNs: (ns) => {
97
- getFallbackArray(ns).forEach((val) => decrementInMap(handlerWrapper.namespaces, val));
98
- return result;
99
- },
100
- subscribeKey: (descriptor) => {
101
- const { key, ns } = descriptor;
102
- incrementInMap(handlerWrapper.keys, key);
103
- getFallbackArray(ns).forEach((val) => incrementInMap(handlerWrapper.namespaces, val));
103
+ getFallbackArray(ns).forEach((val) => handlerWrapper.namespaces.add(val));
104
104
  if (ns === undefined) {
105
- // subscribing to all namespaces
106
- incrementInMap(handlerWrapper.namespaces, undefined);
107
- }
108
- return result;
109
- },
110
- unsubscribeKey: (descriptor) => {
111
- const { key, ns } = descriptor;
112
- decrementInMap(handlerWrapper.keys, key);
113
- getFallbackArray(ns).forEach((val) => decrementInMap(handlerWrapper.namespaces, val));
114
- if (ns === undefined) {
115
- // subscribing to all namespaces
116
- decrementInMap(handlerWrapper.namespaces, undefined);
105
+ // subscribing to default ns
106
+ handlerWrapper.namespaces.add(getDefaultNs());
117
107
  }
118
108
  return result;
119
109
  },
120
110
  };
121
111
  return result;
122
112
  };
123
- const callHandlers = (key, ns) => {
113
+ const callHandlers = (ns) => {
114
+ // everything is implicitly subscribed to fallbacks
115
+ // as it can always fall through to it
116
+ const fallbackNamespaces = new Set(getFallbackNs());
124
117
  partialListeners.forEach((handler) => {
125
- const nsMentioned = ns !== undefined;
126
- const nsMatches = handler.namespaces.has(undefined) ||
127
- (ns === null || ns === void 0 ? void 0 : ns.findIndex((ns) => handler.namespaces.has(ns))) !== -1;
128
- const keyMentioned = key !== undefined;
129
- const keyMatches = key === undefined || handler.keys.has(key) || handler.keys.size === 0;
130
- if ((!nsMentioned || nsMatches) && (!keyMentioned || keyMatches)) {
118
+ const nsMatches = ns === undefined ||
119
+ (ns === null || ns === void 0 ? void 0 : ns.findIndex((ns) => fallbackNamespaces.has(ns) || handler.namespaces.has(ns))) !== -1;
120
+ if (nsMatches) {
131
121
  handler.fn({ value: undefined });
132
122
  }
133
123
  });
134
124
  };
135
125
  let queue = [];
126
+ // merge events in queue into one event
136
127
  const solveQueue = () => {
137
128
  if (queue.length === 0) {
138
129
  return;
139
130
  }
131
+ const queueCopy = queue;
132
+ queue = [];
140
133
  listeners.forEach((handler) => {
141
134
  handler({ value: undefined });
142
135
  });
143
- let namespaces = [];
144
- let keys = [];
145
- queue.forEach((descriptor) => {
146
- if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.ns) === undefined) {
136
+ let namespaces = new Set();
137
+ queueCopy.forEach((ns) => {
138
+ if (ns === undefined) {
139
+ // when no ns specified, it affects all namespaces
147
140
  namespaces = undefined;
148
141
  }
149
142
  else if (namespaces !== undefined) {
150
- namespaces = [...namespaces, ...descriptor.ns];
151
- }
152
- if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.key) === undefined) {
153
- keys = undefined;
143
+ ns.forEach((ns) => namespaces.add(ns));
154
144
  }
155
- else if (keys !== undefined) {
156
- keys = [...keys, descriptor.key];
157
- }
158
- });
159
- (keys || [undefined]).forEach((key) => {
160
- callHandlers(key, namespaces);
161
145
  });
162
- queue = [];
146
+ const namespacesArray = namespaces
147
+ ? Array.from(namespaces.keys())
148
+ : undefined;
149
+ callHandlers(namespacesArray);
163
150
  };
164
- const emit = (descriptor, delayed) => {
165
- queue.push(descriptor);
166
- if (!delayed) {
167
- solveQueue();
168
- }
169
- else {
170
- Promise.resolve().then(() => {
151
+ const emit = (ns, delayed) => {
152
+ if (isActive()) {
153
+ queue.push(ns);
154
+ if (!delayed) {
171
155
  solveQueue();
172
- });
156
+ }
157
+ else {
158
+ setTimeout(solveQueue, 0);
159
+ }
173
160
  }
174
161
  };
175
162
  return Object.freeze({ listenSome, listen, emit });
176
163
  };
177
164
 
178
- const Events = () => {
179
- const onPendingLanguageChange = EventEmitter();
180
- const onLanguageChange = EventEmitter();
181
- const onKeyChange = EventEmitter();
182
- const onLoadingChange = EventEmitter();
183
- const onFetchingChange = EventEmitter();
184
- const onInitialLoaded = EventEmitter();
185
- const onKeyUpdate = EventEmitterSelective();
186
- const onCacheChange = EventEmitter();
187
- const onRunningChange = EventEmitter();
188
- onInitialLoaded.listen(() => onKeyUpdate.emit());
189
- onLanguageChange.listen(() => onKeyUpdate.emit());
165
+ const Events = (getFallbackNs, getDefaultNs) => {
166
+ let emitterActive = true;
167
+ function isActive() {
168
+ return emitterActive;
169
+ }
170
+ const onPendingLanguageChange = EventEmitter(isActive);
171
+ const onLanguageChange = EventEmitter(isActive);
172
+ const onLoadingChange = EventEmitter(isActive);
173
+ const onFetchingChange = EventEmitter(isActive);
174
+ const onInitialLoaded = EventEmitter(isActive);
175
+ const onRunningChange = EventEmitter(isActive);
176
+ const onCacheChange = EventEmitter(isActive);
177
+ const onUpdate = EventEmitterSelective(isActive, getFallbackNs, getDefaultNs);
178
+ onInitialLoaded.listen(() => onUpdate.emit());
179
+ onLanguageChange.listen(() => onUpdate.emit());
190
180
  onCacheChange.listen(({ value }) => {
191
- onKeyUpdate.emit({ ns: [value.namespace], key: value.key }, true);
181
+ onUpdate.emit([value.namespace], true);
192
182
  });
193
183
  const on = (event, handler) => {
194
184
  switch (event) {
@@ -206,20 +196,23 @@
206
196
  return onRunningChange.listen(handler);
207
197
  case 'cache':
208
198
  return onCacheChange.listen(handler);
209
- case 'keyUpdate':
210
- return onKeyUpdate.listen(handler);
199
+ case 'update':
200
+ return onUpdate.listen(handler);
211
201
  }
212
202
  };
203
+ function setEmmiterActive(active) {
204
+ emitterActive = active;
205
+ }
213
206
  return Object.freeze({
214
207
  onPendingLanguageChange,
215
208
  onLanguageChange,
216
- onKeyChange,
217
- onKeyUpdate,
218
209
  onLoadingChange,
219
210
  onFetchingChange,
220
211
  onInitialLoaded,
221
212
  onRunningChange,
222
213
  onCacheChange,
214
+ onUpdate,
215
+ setEmmiterActive,
223
216
  on,
224
217
  });
225
218
  };
@@ -232,7 +225,7 @@
232
225
  return;
233
226
  }
234
227
  if (typeof value === 'object') {
235
- Object.entries(flattenTranslations(value)).forEach(([flatKey, flatValue]) => {
228
+ flattenTranslations(value).forEach((flatValue, flatKey) => {
236
229
  result.set(key + '.' + flatKey, flatValue);
237
230
  });
238
231
  return;
@@ -242,7 +235,9 @@
242
235
  return result;
243
236
  };
244
237
  const decodeCacheKey = (key) => {
245
- const [firstPart, secondPart] = key.split(':');
238
+ const [firstPart, ...rest] = key.split(':');
239
+ // if namespaces contains ":" it won't get lost
240
+ const secondPart = rest.join(':');
246
241
  return { language: firstPart, namespace: secondPart || '' };
247
242
  };
248
243
  const encodeCacheKey = ({ language, namespace, }) => {
@@ -274,13 +269,14 @@
274
269
  }
275
270
  }
276
271
  function invalidate() {
272
+ asyncRequests.clear();
277
273
  version += 1;
278
274
  }
279
- function addRecordInternal(descriptor, data, version) {
275
+ function addRecordInternal(descriptor, data, recordVersion) {
280
276
  const cacheKey = encodeCacheKey(descriptor);
281
277
  cache.set(cacheKey, {
282
278
  data: flattenTranslations(data),
283
- version: version,
279
+ version: recordVersion,
284
280
  });
285
281
  onCacheChange.emit(descriptor);
286
282
  }
@@ -296,7 +292,7 @@
296
292
  }
297
293
  function getRecord(descriptor) {
298
294
  var _a;
299
- return (_a = cache.get(encodeCacheKey(descriptor))) === null || _a === void 0 ? void 0 : _a.data;
295
+ return (_a = cache.get(encodeCacheKey(withDefaultNs(descriptor)))) === null || _a === void 0 ? void 0 : _a.data;
300
296
  }
301
297
  function getTranslation(descriptor, key) {
302
298
  var _a;
@@ -309,11 +305,11 @@
309
305
  const value = (_a = cache
310
306
  .get(encodeCacheKey({ language, namespace }))) === null || _a === void 0 ? void 0 : _a.data.get(key);
311
307
  if (value !== undefined && value !== null) {
312
- return namespace;
308
+ return [namespace];
313
309
  }
314
310
  }
315
311
  }
316
- return Array.from(new Set(namespaces));
312
+ return unique(namespaces);
317
313
  }
318
314
  function getTranslationFallback(namespaces, languages, key) {
319
315
  var _a;
@@ -334,9 +330,6 @@
334
330
  record === null || record === void 0 ? void 0 : record.set(key, value);
335
331
  onCacheChange.emit(Object.assign(Object.assign({}, descriptor), { key }));
336
332
  }
337
- function clear() {
338
- cache.clear();
339
- }
340
333
  function isFetching(ns) {
341
334
  if (isInitialLoading()) {
342
335
  return true;
@@ -359,24 +352,20 @@
359
352
  }));
360
353
  }));
361
354
  }
362
- function fetchNormal(keyObject) {
355
+ /**
356
+ * Fetches production data
357
+ */
358
+ function fetchProd(keyObject) {
363
359
  let dataPromise = undefined;
364
360
  if (!dataPromise) {
365
361
  const staticDataValue = staticData[encodeCacheKey(keyObject)];
366
362
  if (typeof staticDataValue === 'function') {
367
363
  dataPromise = staticDataValue();
368
364
  }
369
- else if (staticDataValue) {
370
- dataPromise = Promise.resolve(staticDataValue);
371
- }
372
365
  }
373
366
  if (!dataPromise) {
374
367
  dataPromise = backendGetRecord(keyObject);
375
368
  }
376
- if (!dataPromise) {
377
- // return empty data, so we know it has already been attempted to fetch
378
- dataPromise = Promise.resolve({});
379
- }
380
369
  return dataPromise;
381
370
  }
382
371
  function fetchData(keyObject, isDev) {
@@ -386,12 +375,12 @@
386
375
  dataPromise = (_a = backendGetDevRecord(keyObject)) === null || _a === void 0 ? void 0 : _a.catch(() => {
387
376
  // eslint-disable-next-line no-console
388
377
  console.warn(`Tolgee: Failed to fetch data from dev backend`);
389
- // fallback to normal fetch if dev fails
390
- return fetchNormal(keyObject);
378
+ // fallback to prod fetch if dev fails
379
+ return fetchProd(keyObject);
391
380
  });
392
381
  }
393
382
  if (!dataPromise) {
394
- dataPromise = fetchNormal(keyObject);
383
+ dataPromise = fetchProd(keyObject);
395
384
  }
396
385
  return dataPromise;
397
386
  }
@@ -408,7 +397,7 @@
408
397
  cacheKey,
409
398
  };
410
399
  }
411
- const dataPromise = fetchData(keyObject, isDev);
400
+ const dataPromise = fetchData(keyObject, isDev) || Promise.resolve(undefined);
412
401
  asyncRequests.set(cacheKey, dataPromise);
413
402
  return {
414
403
  new: true,
@@ -421,12 +410,19 @@
421
410
  loadingObserver.notify();
422
411
  const results = await Promise.all(withPromises.map((val) => val.promise));
423
412
  withPromises.forEach((value, i) => {
424
- if (value.new) {
413
+ const promiseChanged = asyncRequests.get(value.cacheKey) !== value.promise;
414
+ // if promise has changed in between, it means cache been invalidated or
415
+ // new data are being fetched
416
+ if (value.new && !promiseChanged) {
425
417
  asyncRequests.delete(value.cacheKey);
426
418
  const data = results[i];
427
419
  if (data) {
428
420
  addRecord(value.keyObject, data);
429
421
  }
422
+ else if (!getRecord(value.keyObject)) {
423
+ // if no data exist, put empty object
424
+ addRecord(value.keyObject, {});
425
+ }
430
426
  }
431
427
  });
432
428
  fetchingObserver.notify();
@@ -452,27 +448,85 @@
452
448
  isFetching,
453
449
  isLoading,
454
450
  loadRecords,
455
- clear,
456
451
  getAllRecords,
457
452
  });
458
453
  };
459
454
 
460
- function isPromise(value) {
461
- return Boolean(value && typeof value.then === 'function');
455
+ /******************************************************************************
456
+ Copyright (c) Microsoft Corporation.
457
+
458
+ Permission to use, copy, modify, and/or distribute this software for any
459
+ purpose with or without fee is hereby granted.
460
+
461
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
462
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
463
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
464
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
465
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
466
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
467
+ PERFORMANCE OF THIS SOFTWARE.
468
+ ***************************************************************************** */
469
+
470
+ function __rest(s, e) {
471
+ var t = {};
472
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
473
+ t[p] = s[p];
474
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
475
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
476
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
477
+ t[p[i]] = s[p[i]];
478
+ }
479
+ return t;
462
480
  }
463
- const valueOrPromise = (value, callback) => {
464
- if (isPromise(value)) {
465
- return Promise.resolve(value).then(callback);
466
- }
467
- else {
468
- return callback(value);
469
- }
481
+
482
+ const defaultObserverOptions = {
483
+ tagAttributes: {
484
+ textarea: ['placeholder'],
485
+ input: ['value', 'placeholder'],
486
+ img: ['alt'],
487
+ '*': ['aria-label', 'title'],
488
+ },
489
+ restrictedElements: ['script', 'style'],
490
+ highlightKeys: ['Alt'],
491
+ highlightColor: 'rgb(255, 0, 0)',
492
+ highlightWidth: 5,
493
+ inputPrefix: '%-%tolgee:',
494
+ inputSuffix: '%-%',
495
+ passToParent: ['option', 'optgroup'],
496
+ };
497
+
498
+ const DEFAULT_FORMAT_ERROR = 'invalid';
499
+ const defaultValues = {
500
+ defaultNs: '',
501
+ observerOptions: defaultObserverOptions,
502
+ observerType: 'invisible',
503
+ onFormatError: DEFAULT_FORMAT_ERROR,
504
+ };
505
+ const combineOptions = (...states) => {
506
+ let result = {};
507
+ states.forEach((state) => {
508
+ result = Object.assign(Object.assign(Object.assign({}, result), state), { observerOptions: Object.assign(Object.assign({}, result.observerOptions), state === null || state === void 0 ? void 0 : state.observerOptions) });
509
+ });
510
+ return result;
511
+ };
512
+ const initState = (options, previousState) => {
513
+ const initialOptions = combineOptions(defaultValues, previousState === null || previousState === void 0 ? void 0 : previousState.initialOptions, options);
514
+ // remove extra '/' from url end
515
+ initialOptions.apiUrl = sanitizeUrl(initialOptions.apiUrl);
516
+ return {
517
+ initialOptions,
518
+ activeNamespaces: (previousState === null || previousState === void 0 ? void 0 : previousState.activeNamespaces) || new Map(),
519
+ language: previousState === null || previousState === void 0 ? void 0 : previousState.language,
520
+ pendingLanguage: previousState === null || previousState === void 0 ? void 0 : previousState.language,
521
+ isInitialLoading: false,
522
+ isRunning: false,
523
+ };
470
524
  };
471
- const missingOptionError = (option) => `Tolgee: You need to specify '${option}' option`;
472
525
 
473
- const PluginService = (getLanguage, getInitialOptions, getAvailableLanguages, getTranslationNs, getTranslation, changeTranslation) => {
526
+ const Plugins = (getLanguage, getInitialOptions, getAvailableLanguages, getTranslationNs, getTranslation, changeTranslation) => {
474
527
  const plugins = {
475
528
  ui: undefined,
529
+ observer: undefined,
476
530
  };
477
531
  const instances = {
478
532
  formatters: [],
@@ -484,39 +538,20 @@
484
538
  languageDetector: undefined,
485
539
  languageStorage: undefined,
486
540
  };
487
- const onClick = async (event, { keysAndDefaults }) => {
541
+ const onClick = async ({ keysAndDefaults, event }) => {
488
542
  var _a;
489
- const withNs = keysAndDefaults.map(({ key, ns, defaultValue }) => ({
490
- key,
491
- defaultValue,
492
- ns: getFallbackArray(getTranslationNs({ key, ns, defaultValue })),
493
- translation: getTranslation({
543
+ const withNs = keysAndDefaults.map(({ key, ns, defaultValue }) => {
544
+ return {
494
545
  key,
495
- ns,
496
- }),
497
- }));
498
- (_a = instances.ui) === null || _a === void 0 ? void 0 : _a.handleElementClick(event, withNs);
499
- };
500
- const run = () => {
501
- var _a;
502
- instances.ui =
503
- plugins.ui &&
504
- new plugins.ui({
505
- apiKey: getInitialOptions().apiKey,
506
- apiUrl: getInitialOptions().apiUrl,
507
- highlight,
508
- changeTranslation,
509
- });
510
- (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.run({ mouseHighlight: Boolean(instances.ui) });
511
- checkCorrectConfiguration();
512
- };
513
- const checkCorrectConfiguration = () => {
514
- if (instances.languageDetector) {
515
- const availableLanguages = getAvailableLanguages();
516
- if (!availableLanguages) {
517
- throw new Error(missingOptionError('availableLanguages'));
518
- }
519
- }
546
+ defaultValue,
547
+ ns: getTranslationNs({ key, ns }),
548
+ translation: getTranslation({
549
+ key,
550
+ ns,
551
+ }),
552
+ };
553
+ });
554
+ (_a = instances.ui) === null || _a === void 0 ? void 0 : _a.handleElementClick(withNs, event);
520
555
  };
521
556
  const stop = () => {
522
557
  var _a;
@@ -528,14 +563,17 @@
528
563
  return ((_b = (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.call(_a, key, ns)) || { unhighlight() { } };
529
564
  };
530
565
  const translate = (props) => {
531
- const translation = getTranslation(props);
566
+ const translation = getTranslation({
567
+ key: props.key,
568
+ ns: props.ns,
569
+ });
532
570
  return formatTranslation(Object.assign(Object.assign({}, props), { translation, formatEnabled: true }));
533
571
  };
534
572
  const setObserver = (observer) => {
535
- instances.observer = observer === null || observer === void 0 ? void 0 : observer({ translate, onClick });
573
+ plugins.observer = observer;
536
574
  };
537
- const getObserver = () => {
538
- return instances.observer;
575
+ const hasObserver = () => {
576
+ return Boolean(plugins.observer);
539
577
  };
540
578
  const addFormatter = (formatter) => {
541
579
  if (formatter) {
@@ -546,14 +584,17 @@
546
584
  instances.finalFormatter = formatter;
547
585
  };
548
586
  const setUi = (ui) => {
549
- plugins.ui = (ui === null || ui === void 0 ? void 0 : ui.UI) || ui;
587
+ plugins.ui = ui;
550
588
  };
551
- const getUi = () => {
552
- return plugins.ui;
589
+ const hasUi = () => {
590
+ return Boolean(plugins.ui);
553
591
  };
554
592
  const setLanguageStorage = (storage) => {
555
593
  instances.languageStorage = storage;
556
594
  };
595
+ const getLanguageStorage = () => {
596
+ return instances.languageStorage;
597
+ };
557
598
  const setStoredLanguage = (language) => {
558
599
  var _a;
559
600
  (_a = instances.languageStorage) === null || _a === void 0 ? void 0 : _a.setLanguage(language);
@@ -561,6 +602,9 @@
561
602
  const setLanguageDetector = (detector) => {
562
603
  instances.languageDetector = detector;
563
604
  };
605
+ const getLanguageDetector = () => {
606
+ return instances.languageDetector;
607
+ };
564
608
  const detectLanguage = () => {
565
609
  if (!instances.languageDetector) {
566
610
  return undefined;
@@ -590,14 +634,37 @@
590
634
  const setDevBackend = (backend) => {
591
635
  instances.devBackend = backend;
592
636
  };
637
+ const run = () => {
638
+ var _a, _b, _c;
639
+ if (!instances.ui) {
640
+ const { apiKey, apiUrl, projectId } = getInitialOptions();
641
+ instances.ui = (_a = plugins.ui) === null || _a === void 0 ? void 0 : _a.call(plugins, {
642
+ apiKey: apiKey,
643
+ apiUrl: apiUrl,
644
+ projectId,
645
+ highlight,
646
+ changeTranslation,
647
+ });
648
+ }
649
+ if (!instances.observer) {
650
+ instances.observer = (_b = plugins.observer) === null || _b === void 0 ? void 0 : _b.call(plugins, {
651
+ translate,
652
+ onClick,
653
+ options: getInitialOptions().observerOptions,
654
+ });
655
+ }
656
+ (_c = instances.observer) === null || _c === void 0 ? void 0 : _c.run({ mouseHighlight: true });
657
+ };
593
658
  const getDevBackend = () => {
594
659
  return instances.devBackend;
595
660
  };
596
661
  const getBackendDevRecord = ({ language, namespace }) => {
597
662
  var _a;
663
+ const { apiKey, apiUrl, projectId } = getInitialOptions();
598
664
  return (_a = instances.devBackend) === null || _a === void 0 ? void 0 : _a.getRecord({
599
- apiKey: getInitialOptions().apiKey,
600
- apiUrl: getInitialOptions().apiUrl,
665
+ apiKey,
666
+ apiUrl,
667
+ projectId,
601
668
  language,
602
669
  namespace,
603
670
  });
@@ -618,42 +685,97 @@
618
685
  }
619
686
  return undefined;
620
687
  };
621
- const formatTranslation = ({ key, translation, defaultValue, noWrap, params, orEmpty, ns, formatEnabled, }) => {
688
+ const unwrap = (text) => {
689
+ var _a;
690
+ if (instances.observer) {
691
+ return (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.unwrap(text);
692
+ }
693
+ return { text, keys: [] };
694
+ };
695
+ const retranslate = () => {
622
696
  var _a;
697
+ (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.retranslate();
698
+ };
699
+ function addPlugin(tolgeeInstance, plugin) {
700
+ const pluginTools = Object.freeze({
701
+ setFinalFormatter,
702
+ addFormatter,
703
+ setObserver,
704
+ hasObserver,
705
+ setUi,
706
+ hasUi,
707
+ setDevBackend,
708
+ addBackend,
709
+ setLanguageDetector,
710
+ setLanguageStorage,
711
+ });
712
+ plugin(tolgeeInstance, pluginTools);
713
+ }
714
+ function formatTranslation(_a) {
715
+ var _b;
716
+ var { formatEnabled } = _a, props = __rest(_a, ["formatEnabled"]);
717
+ const { key, translation, defaultValue, noWrap, params, orEmpty, ns } = props;
623
718
  const formattableTranslation = translation || defaultValue;
624
719
  let result = formattableTranslation || (orEmpty ? '' : key);
625
- if (instances.observer && !noWrap) {
626
- result = instances.observer.wrap({
627
- key,
628
- translation: result,
629
- defaultValue,
630
- params,
631
- ns,
632
- });
633
- }
634
720
  const language = getLanguage();
635
- const isFormatEnabled = formatEnabled || !((_a = instances.observer) === null || _a === void 0 ? void 0 : _a.outputNotFormattable);
636
- if (formattableTranslation && language && isFormatEnabled) {
637
- for (const formatter of instances.formatters) {
638
- result = formatter.format({
721
+ const isFormatEnabled = formatEnabled || !((_b = instances.observer) === null || _b === void 0 ? void 0 : _b.outputNotFormattable);
722
+ const wrap = (result) => {
723
+ if (instances.observer && !noWrap) {
724
+ return instances.observer.wrap({
725
+ key,
726
+ translation: result,
727
+ defaultValue,
728
+ params,
729
+ ns,
730
+ });
731
+ }
732
+ return result;
733
+ };
734
+ result = wrap(result);
735
+ try {
736
+ if (formattableTranslation && language && isFormatEnabled) {
737
+ for (const formatter of instances.formatters) {
738
+ result = formatter.format({
739
+ translation: result,
740
+ language,
741
+ params,
742
+ });
743
+ }
744
+ }
745
+ if (instances.finalFormatter &&
746
+ formattableTranslation &&
747
+ language &&
748
+ isFormatEnabled) {
749
+ result = instances.finalFormatter.format({
639
750
  translation: result,
640
751
  language,
641
752
  params,
642
753
  });
643
754
  }
644
755
  }
645
- if (instances.finalFormatter &&
646
- formattableTranslation &&
647
- language &&
648
- isFormatEnabled) {
649
- result = instances.finalFormatter.format({
650
- translation: result,
651
- language,
652
- params,
653
- });
756
+ catch (e) {
757
+ // eslint-disable-next-line no-console
758
+ console.error(e);
759
+ const errorMessage = getErrorMessage(e) || DEFAULT_FORMAT_ERROR;
760
+ const onFormatError = getInitialOptions().onFormatError;
761
+ const formatErrorType = typeof onFormatError;
762
+ if (formatErrorType === 'string') {
763
+ result = onFormatError;
764
+ }
765
+ else if (formatErrorType === 'function') {
766
+ result = onFormatError(errorMessage, props);
767
+ }
768
+ else {
769
+ result = DEFAULT_FORMAT_ERROR;
770
+ }
771
+ // wrap error message, so it's detectable
772
+ result = wrap(result);
654
773
  }
655
774
  return result;
656
- };
775
+ }
776
+ function hasDevBackend() {
777
+ return Boolean(getDevBackend());
778
+ }
657
779
  const wrap = (params) => {
658
780
  var _a;
659
781
  if (instances.observer) {
@@ -661,40 +783,23 @@
661
783
  }
662
784
  return params.translation;
663
785
  };
664
- const unwrap = (text) => {
665
- var _a;
666
- if (instances.observer) {
667
- return (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.unwrap(text);
668
- }
669
- return { text, keys: [] };
670
- };
671
- const retranslate = () => {
672
- var _a;
673
- (_a = instances.observer) === null || _a === void 0 ? void 0 : _a.retranslate();
674
- };
675
786
  return Object.freeze({
676
- setFinalFormatter,
677
- addFormatter,
787
+ addPlugin,
678
788
  formatTranslation,
679
- setObserver,
680
- getObserver,
681
- setUi,
682
- getUi,
683
- addBackend,
684
- setDevBackend,
685
789
  getDevBackend,
686
790
  getBackendRecord,
687
791
  getBackendDevRecord,
688
- setLanguageDetector,
689
- setLanguageStorage,
792
+ getLanguageDetector,
793
+ getLanguageStorage,
690
794
  getInitialLanguage,
691
795
  setStoredLanguage,
692
796
  run,
693
797
  stop,
694
798
  retranslate,
695
799
  highlight,
696
- wrap,
697
800
  unwrap,
801
+ wrap,
802
+ hasDevBackend,
698
803
  });
699
804
  };
700
805
 
@@ -716,28 +821,9 @@
716
821
  });
717
822
  };
718
823
 
719
- const defaultValues = {
720
- enableLanguageStore: true,
721
- defaultNs: '',
722
- filesUrlPrefix: 'i18n/',
723
- };
724
- const initState = (options, previousState) => {
725
- const initialOptions = Object.assign(Object.assign(Object.assign({}, defaultValues), previousState === null || previousState === void 0 ? void 0 : previousState.initialOptions), options);
726
- // remove extra '/' from url end
727
- const apiUrl = initialOptions.apiUrl;
728
- initialOptions.apiUrl = apiUrl ? apiUrl.replace(/\/+$/, '') : apiUrl;
729
- return {
730
- initialOptions,
731
- activeNamespaces: (previousState === null || previousState === void 0 ? void 0 : previousState.activeNamespaces) || new Map(),
732
- language: previousState === null || previousState === void 0 ? void 0 : previousState.language,
733
- pendingLanguage: previousState === null || previousState === void 0 ? void 0 : previousState.language,
734
- isInitialLoading: false,
735
- isRunning: false,
736
- };
737
- };
738
-
739
824
  const State = (onLanguageChange, onPendingLanguageChange, onRunningChange) => {
740
825
  let state = initState();
826
+ let devCredentials = undefined;
741
827
  function init(options) {
742
828
  state = initState(options, state);
743
829
  }
@@ -759,13 +845,6 @@
759
845
  function getLanguage() {
760
846
  return state.language || state.initialOptions.language;
761
847
  }
762
- function getLanguageOrFail() {
763
- const language = state.language || state.initialOptions.language;
764
- if (!language) {
765
- throw new Error(`No language set`);
766
- }
767
- return language;
768
- }
769
848
  function setLanguage(language) {
770
849
  if (state.language !== language) {
771
850
  state.language = language;
@@ -782,7 +861,7 @@
782
861
  }
783
862
  }
784
863
  function getInitialOptions() {
785
- return state.initialOptions;
864
+ return Object.assign(Object.assign({}, state.initialOptions), devCredentials);
786
865
  }
787
866
  function addActiveNs(ns) {
788
867
  const namespaces = getFallbackArray(ns);
@@ -811,6 +890,7 @@
811
890
  function getRequiredNamespaces() {
812
891
  return unique([
813
892
  ...(state.initialOptions.ns || [state.initialOptions.defaultNs]),
893
+ ...getFallbackArray(state.initialOptions.fallbackNs),
814
894
  ...state.activeNamespaces.keys(),
815
895
  ]);
816
896
  }
@@ -824,11 +904,11 @@
824
904
  ...getFallbackFromStruct(language, state.initialOptions.fallbackLanguage),
825
905
  ]);
826
906
  }
827
- function getFallbackNamespaces() {
828
- const defaultNs = state.initialOptions.defaultNs;
829
- const fallbackNs = state.initialOptions.fallbackNs;
830
- const fallbackNamespaces = typeof defaultNs === 'string' ? [defaultNs] : [];
831
- return unique([...fallbackNamespaces, ...getFallbackArray(fallbackNs)]);
907
+ function getFallbackNs() {
908
+ return getFallbackArray(state.initialOptions.fallbackNs);
909
+ }
910
+ function getDefaultNs(ns) {
911
+ return ns === undefined ? state.initialOptions.defaultNs : ns;
832
912
  }
833
913
  function getAvailableLanguages() {
834
914
  if (state.initialOptions.availableLanguages) {
@@ -847,6 +927,14 @@
847
927
  language: descriptor.language,
848
928
  };
849
929
  }
930
+ function overrideCredentials(credentials) {
931
+ if (credentials) {
932
+ devCredentials = Object.assign(Object.assign({}, credentials), { apiUrl: sanitizeUrl(credentials.apiUrl) });
933
+ }
934
+ else {
935
+ devCredentials = undefined;
936
+ }
937
+ }
850
938
  return Object.freeze({
851
939
  init,
852
940
  isRunning,
@@ -854,7 +942,6 @@
854
942
  isInitialLoading,
855
943
  setInitialLoading,
856
944
  getLanguage,
857
- getLanguageOrFail,
858
945
  setLanguage,
859
946
  getPendingLanguage,
860
947
  setPendingLanguage,
@@ -863,50 +950,24 @@
863
950
  removeActiveNs,
864
951
  getRequiredNamespaces,
865
952
  getFallbackLangs,
866
- getFallbackNamespaces,
953
+ getFallbackNs,
954
+ getDefaultNs,
867
955
  getAvailableLanguages,
868
956
  withDefaultNs,
957
+ overrideCredentials,
869
958
  });
870
959
  };
871
960
 
872
- /******************************************************************************
873
- Copyright (c) Microsoft Corporation.
874
-
875
- Permission to use, copy, modify, and/or distribute this software for any
876
- purpose with or without fee is hereby granted.
877
-
878
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
879
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
880
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
881
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
882
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
883
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
884
- PERFORMANCE OF THIS SOFTWARE.
885
- ***************************************************************************** */
886
-
887
- function __rest(s, e) {
888
- var t = {};
889
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
890
- t[p] = s[p];
891
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
892
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
893
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
894
- t[p[i]] = s[p[i]];
895
- }
896
- return t;
897
- }
898
-
899
961
  function parseCombinedOptions(_a) {
900
962
  var { ns, noWrap, orEmpty, params } = _a, rest = __rest(_a, ["ns", "noWrap", "orEmpty", "params"]);
901
963
  const options = {
902
964
  ns: ns,
903
965
  noWrap: noWrap,
904
966
  orEmpty: orEmpty,
905
- params: Object.assign(Object.assign({}, rest), params),
906
967
  };
907
- return options;
968
+ return Object.assign(Object.assign({}, options), { params: Object.assign({}, rest) });
908
969
  }
909
- const getTranslateParams = (keyOrProps, ...params) => {
970
+ const getTranslateProps = (keyOrProps, ...params) => {
910
971
  let result = {};
911
972
  let options;
912
973
  if (typeof keyOrProps === 'object') {
@@ -928,28 +989,40 @@
928
989
  return result;
929
990
  };
930
991
 
931
- const Controller = ({ events, options }) => {
992
+ const Controller = ({ options }) => {
993
+ const events = Events(getFallbackNs, getDefaultNs);
932
994
  const fetchingObserver = ValueObserver(false, () => cache.isFetching(), events.onFetchingChange.emit);
933
995
  const loadingObserver = ValueObserver(false, () => isLoading(), events.onLoadingChange.emit);
934
996
  const state = State(events.onLanguageChange, events.onPendingLanguageChange, events.onRunningChange);
935
- const pluginService = PluginService(state.getLanguage, state.getInitialOptions, state.getAvailableLanguages, getTranslationNs, getTranslation, changeTranslation);
997
+ const pluginService = Plugins(state.getLanguage, state.getInitialOptions, state.getAvailableLanguages, getTranslationNs, getTranslation, changeTranslation);
936
998
  const cache = Cache(events.onCacheChange, pluginService.getBackendRecord, pluginService.getBackendDevRecord, state.withDefaultNs, state.isInitialLoading, fetchingObserver, loadingObserver);
937
- state.init(options);
938
- cache.addStaticData(state.getInitialOptions().staticData);
939
- if (isDev()) {
940
- cache.invalidate();
999
+ if (options) {
1000
+ init(options);
941
1001
  }
942
- events.onKeyUpdate.listen(() => {
1002
+ events.onUpdate.listen(() => {
943
1003
  if (state.isRunning()) {
944
1004
  pluginService.retranslate();
945
1005
  }
946
1006
  });
947
- const t = (...args) => {
948
- // @ts-ignore
949
- const params = getTranslateParams(...args);
950
- const translation = getTranslation(params);
951
- return pluginService.formatTranslation(Object.assign(Object.assign({}, params), { translation }));
952
- };
1007
+ function getFallbackNs() {
1008
+ return state.getFallbackNs();
1009
+ }
1010
+ function getDefaultNs(ns) {
1011
+ return state.getDefaultNs(ns);
1012
+ }
1013
+ // gets all namespaces where translation could be located
1014
+ // takes (ns|default, fallback ns)
1015
+ function getDefaultAndFallbackNs(ns) {
1016
+ return [...getFallbackArray(getDefaultNs(ns)), ...getFallbackNs()];
1017
+ }
1018
+ // gets all namespaces which need to be loaded
1019
+ // takes (ns|default, initial ns, fallback ns, active ns)
1020
+ function getRequiredNamespaces(ns) {
1021
+ return [
1022
+ ...getFallbackArray(ns || getDefaultNs()),
1023
+ ...state.getRequiredNamespaces(),
1024
+ ];
1025
+ }
953
1026
  function changeTranslation(descriptor, key, value) {
954
1027
  const keyObject = state.withDefaultNs(descriptor);
955
1028
  const previousValue = cache.getTranslation(keyObject, key);
@@ -963,15 +1036,12 @@
963
1036
  function init(options) {
964
1037
  state.init(options);
965
1038
  cache.addStaticData(state.getInitialOptions().staticData);
966
- if (isDev()) {
967
- cache.invalidate();
968
- }
969
1039
  }
970
1040
  function isLoading(ns) {
971
1041
  return cache.isLoading(state.getLanguage(), ns);
972
1042
  }
973
1043
  function isDev() {
974
- return Boolean(state.getInitialOptions().apiKey && pluginService.getDevBackend());
1044
+ return Boolean(state.getInitialOptions().apiKey && state.getInitialOptions().apiUrl);
975
1045
  }
976
1046
  async function addActiveNs(ns, forget) {
977
1047
  if (!forget) {
@@ -983,7 +1053,7 @@
983
1053
  }
984
1054
  function getRequiredRecords(lang, ns) {
985
1055
  const languages = state.getFallbackLangs(lang);
986
- const namespaces = ns !== undefined ? getFallbackArray(ns) : state.getRequiredNamespaces();
1056
+ const namespaces = getRequiredNamespaces(ns);
987
1057
  const result = [];
988
1058
  languages.forEach((language) => {
989
1059
  namespaces.forEach((namespace) => {
@@ -1000,7 +1070,7 @@
1000
1070
  return false;
1001
1071
  }
1002
1072
  const languages = state.getFallbackLangs(language);
1003
- const namespaces = ns !== undefined ? getFallbackArray(ns) : state.getRequiredNamespaces();
1073
+ const namespaces = getRequiredNamespaces(ns);
1004
1074
  const result = [];
1005
1075
  languages.forEach((language) => {
1006
1076
  namespaces.forEach((namespace) => {
@@ -1033,24 +1103,19 @@
1033
1103
  pluginService.setStoredLanguage(language);
1034
1104
  }
1035
1105
  }
1036
- function getTranslationNs({ key, ns, }) {
1037
- const namespaces = ns
1038
- ? getFallbackArray(ns)
1039
- : state.getFallbackNamespaces();
1106
+ function getTranslationNs({ key, ns }) {
1040
1107
  const languages = state.getFallbackLangs();
1108
+ const namespaces = getDefaultAndFallbackNs(ns);
1041
1109
  return cache.getTranslationNs(namespaces, languages, key);
1042
1110
  }
1043
- function getTranslation({ key, ns, }) {
1044
- const namespaces = ns
1045
- ? getFallbackArray(ns)
1046
- : state.getFallbackNamespaces();
1111
+ function getTranslation({ key, ns }) {
1112
+ const namespaces = getDefaultAndFallbackNs(ns);
1047
1113
  const languages = state.getFallbackLangs();
1048
1114
  return cache.getTranslationFallback(namespaces, languages, key);
1049
1115
  }
1050
1116
  function loadInitial() {
1051
1117
  const data = valueOrPromise(initializeLanguage(), () => {
1052
1118
  // fail if there is no language
1053
- state.getLanguageOrFail();
1054
1119
  return loadRequiredRecords();
1055
1120
  });
1056
1121
  if (isPromise(data)) {
@@ -1089,9 +1154,30 @@
1089
1154
  function loadRecords(descriptors) {
1090
1155
  return cache.loadRecords(descriptors, isDev());
1091
1156
  }
1157
+ const checkCorrectConfiguration = () => {
1158
+ const languageComputable = pluginService.getLanguageDetector() || pluginService.getLanguageStorage();
1159
+ if (languageComputable) {
1160
+ const availableLanguages = state.getAvailableLanguages();
1161
+ if (!availableLanguages) {
1162
+ throw new Error(missingOptionError('availableLanguages'));
1163
+ }
1164
+ }
1165
+ if (!state.getLanguage() && !state.getInitialOptions().defaultLanguage) {
1166
+ if (languageComputable) {
1167
+ throw new Error(missingOptionError('defaultLanguage'));
1168
+ }
1169
+ else {
1170
+ throw new Error(missingOptionError('language'));
1171
+ }
1172
+ }
1173
+ };
1092
1174
  function run() {
1093
1175
  let result = undefined;
1176
+ checkCorrectConfiguration();
1094
1177
  if (!state.isRunning()) {
1178
+ if (isDev()) {
1179
+ cache.invalidate();
1180
+ }
1095
1181
  state.setRunning(true);
1096
1182
  pluginService.run();
1097
1183
  result = loadInitial();
@@ -1104,12 +1190,17 @@
1104
1190
  state.setRunning(false);
1105
1191
  }
1106
1192
  }
1107
- return Object.freeze(Object.assign(Object.assign(Object.assign(Object.assign({}, state), pluginService), cache), { init,
1193
+ const t = (...args) => {
1194
+ // @ts-ignore
1195
+ const params = getTranslateProps(...args);
1196
+ const translation = getTranslation(params);
1197
+ return pluginService.formatTranslation(Object.assign(Object.assign({}, params), { translation }));
1198
+ };
1199
+ return Object.freeze(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, events), state), pluginService), cache), { init,
1108
1200
  changeLanguage,
1109
1201
  getTranslation,
1110
1202
  changeTranslation,
1111
1203
  addActiveNs,
1112
- loadRequiredRecords,
1113
1204
  loadRecords,
1114
1205
  loadRecord,
1115
1206
  isLoading,
@@ -1120,24 +1211,11 @@
1120
1211
  stop }));
1121
1212
  };
1122
1213
 
1123
- const Tolgee = (options) => {
1124
- const events = Events();
1214
+ const createTolgee = (options) => {
1125
1215
  const controller = Controller({
1126
- events,
1127
1216
  options,
1128
1217
  });
1129
- const pluginTools = Object.freeze({
1130
- setFinalFormatter: controller.setFinalFormatter,
1131
- addFormatter: controller.addFormatter,
1132
- setObserver: controller.setObserver,
1133
- getObserver: controller.getObserver,
1134
- setUi: controller.setUi,
1135
- getUi: controller.getUi,
1136
- setDevBackend: controller.setDevBackend,
1137
- addBackend: controller.addBackend,
1138
- setLanguageDetector: controller.setLanguageDetector,
1139
- setLanguageStorage: controller.setLanguageStorage,
1140
- });
1218
+ // restarts tolgee while applying callback
1141
1219
  const withRestart = (callback) => {
1142
1220
  const wasRunning = controller.isRunning();
1143
1221
  wasRunning && controller.stop();
@@ -1145,65 +1223,363 @@
1145
1223
  wasRunning && controller.run();
1146
1224
  };
1147
1225
  const tolgee = Object.freeze({
1148
- // event listeners
1149
- on: events.on,
1150
- onKeyUpdate: events.onKeyUpdate.listenSome,
1151
- // state
1226
+ /**
1227
+ * Listen to tolgee events.
1228
+ */
1229
+ on: controller.on,
1230
+ /**
1231
+ * Listen for specific namespaces changes.
1232
+ *
1233
+ * ```
1234
+ * const sub = tolgee.onUpdate(handler)
1235
+ *
1236
+ * // subscribe to selected namespace
1237
+ * sub.subscribeNs(['common'])
1238
+ *
1239
+ * // unsubscribe
1240
+ * sub.unsubscribe()
1241
+ * ```
1242
+ */
1243
+ onNsUpdate: controller.onUpdate.listenSome,
1244
+ /**
1245
+ * Turn off/on events emitting. Is on by default.
1246
+ */
1247
+ setEmmiterActive: controller.setEmmiterActive,
1248
+ /**
1249
+ * @return current language if set.
1250
+ */
1152
1251
  getLanguage: controller.getLanguage,
1252
+ /**
1253
+ * `pendingLanguage` represents language which is currently being loaded.
1254
+ * @return current `pendingLanguage` if set.
1255
+ */
1153
1256
  getPendingLanguage: controller.getPendingLanguage,
1257
+ /**
1258
+ * Change current language.
1259
+ * - if not running sets `pendingLanguage`, `language` to the new value
1260
+ * - if running sets `pendingLanguage` to the value, fetches necessary data and then changes `language`
1261
+ *
1262
+ * @return Promise which is resolved when `language` is changed.
1263
+ */
1154
1264
  changeLanguage: controller.changeLanguage,
1265
+ /**
1266
+ * Temporarily change translation in cache.
1267
+ * @return object with revert method.
1268
+ */
1155
1269
  changeTranslation: controller.changeTranslation,
1270
+ /**
1271
+ * Adds namespace(s) list of active namespaces. And if tolgee is running, loads required data.
1272
+ */
1156
1273
  addActiveNs: controller.addActiveNs,
1274
+ /**
1275
+ * Remove namespace(s) from active namespaces.
1276
+ *
1277
+ * Tolgee internally counts how many times was each active namespace added,
1278
+ * so this method will remove namespace only if the counter goes down to 0.
1279
+ */
1157
1280
  removeActiveNs: controller.removeActiveNs,
1281
+ /**
1282
+ * Manually load multiple records from `Backend` (or `DevBackend` when in dev mode)
1283
+ *
1284
+ * It loads data together and adds them to cache in one operation, to prevent partly loaded state.
1285
+ */
1158
1286
  loadRecords: controller.loadRecords,
1287
+ /**
1288
+ * Manually load record from `Backend` (or `DevBackend` when in dev mode)
1289
+ */
1159
1290
  loadRecord: controller.loadRecord,
1291
+ /**
1292
+ *
1293
+ */
1160
1294
  addStaticData: controller.addStaticData,
1295
+ /**
1296
+ * Get record from cache.
1297
+ */
1161
1298
  getRecord: controller.getRecord,
1299
+ /**
1300
+ * Get all records from cache.
1301
+ */
1162
1302
  getAllRecords: controller.getAllRecords,
1303
+ /**
1304
+ * @param ns optional list of namespaces that you are interested in
1305
+ * @return `true` if there are data that need to be fetched.
1306
+ */
1163
1307
  isLoaded: controller.isLoaded,
1308
+ /**
1309
+ * @return `true` if tolgee is loading initial data (triggered by `run`).
1310
+ */
1164
1311
  isInitialLoading: controller.isInitialLoading,
1312
+ /**
1313
+ * @param ns optional list of namespaces that you are interested in
1314
+ * @return `true` if tolgee is loading some translations for the first time.
1315
+ */
1165
1316
  isLoading: controller.isLoading,
1317
+ /**
1318
+ * @param ns optional list of namespaces that you are interested in
1319
+ * @return `true` if tolgee is fetching some translations.
1320
+ */
1166
1321
  isFetching: controller.isFetching,
1322
+ /**
1323
+ * @return `true` if tolgee is running.
1324
+ */
1167
1325
  isRunning: controller.isRunning,
1326
+ /**
1327
+ * Changes internal state to running: true and loads initial files.
1328
+ * Runs runnable plugins mainly Observer if present.
1329
+ */
1168
1330
  run: controller.run,
1331
+ /**
1332
+ * Changes internal state to running: false and stops runnable plugins.
1333
+ */
1169
1334
  stop: controller.stop,
1335
+ /**
1336
+ * Returns translated and formatted key.
1337
+ * If Observer is present and tolgee is running, wraps result to be identifiable in the DOM.
1338
+ */
1170
1339
  t: controller.t,
1340
+ /**
1341
+ * Highlight keys that match selection.
1342
+ */
1171
1343
  highlight: controller.highlight,
1344
+ /**
1345
+ * @return current Tolgee options.
1346
+ */
1172
1347
  getInitialOptions: controller.getInitialOptions,
1348
+ /**
1349
+ * Tolgee is in dev mode if `DevTools` plugin is used and `apiKey` + `apiUrl` are specified.
1350
+ * @return `true` if tolgee is in dev mode.
1351
+ */
1173
1352
  isDev: controller.isDev,
1353
+ /**
1354
+ * Wraps translation if there is `Observer` plugin
1355
+ */
1174
1356
  wrap: controller.wrap,
1357
+ /**
1358
+ * Unwrap translation
1359
+ */
1175
1360
  unwrap: controller.unwrap,
1176
- // plugins
1177
- use: (plugin) => {
1361
+ /**
1362
+ * Override creadentials passed on initialization.
1363
+ *
1364
+ * When called in running state, tolgee stops and runs again.
1365
+ */
1366
+ overrideCredentials(credentials) {
1367
+ withRestart(() => controller.overrideCredentials(credentials));
1368
+ },
1369
+ /**
1370
+ * Add tolgee plugin after initialization.
1371
+ *
1372
+ * When called in running state, tolgee stops and runs again.
1373
+ */
1374
+ addPlugin(plugin) {
1178
1375
  if (plugin) {
1179
- withRestart(() => plugin(tolgee, pluginTools));
1376
+ withRestart(() => controller.addPlugin(tolgee, plugin));
1180
1377
  }
1181
- return tolgee;
1182
1378
  },
1183
- init: (options) => {
1184
- withRestart(() => controller.init(options));
1185
- return tolgee;
1379
+ /**
1380
+ * Updates options after instance creation. Extends existing options,
1381
+ * so it only changes the fields, that are listed.
1382
+ *
1383
+ * When called in running state, tolgee stops and runs again.
1384
+ */
1385
+ updateOptions(options) {
1386
+ if (options) {
1387
+ withRestart(() => controller.init(options));
1388
+ }
1186
1389
  },
1187
1390
  });
1188
1391
  return tolgee;
1189
1392
  };
1393
+ /**
1394
+ * Tolgee chainable constructor.
1395
+ *
1396
+ * Usage:
1397
+ * ```
1398
+ * const tolgee = Tolgee().use(...).init(...)
1399
+ * ```
1400
+ */
1401
+ const TolgeeCore = () => {
1402
+ const state = {
1403
+ plugins: [],
1404
+ options: {},
1405
+ };
1406
+ const tolgeeChain = Object.freeze({
1407
+ use(plugin) {
1408
+ state.plugins.push(plugin);
1409
+ return tolgeeChain;
1410
+ },
1411
+ updateDefaults(options) {
1412
+ state.options = combineOptions(state.options, options);
1413
+ return tolgeeChain;
1414
+ },
1415
+ init(options) {
1416
+ const tolgee = createTolgee(combineOptions(state.options, options));
1417
+ state.plugins.forEach(tolgee.addPlugin);
1418
+ return tolgee;
1419
+ },
1420
+ });
1421
+ return tolgeeChain;
1422
+ };
1190
1423
 
1191
- const RESTRICTED_ASCENDANT_ATTRIBUTE = 'data-tolgee-restricted';
1192
- const TOLGEE_ATTRIBUTE_NAME = '_tolgee';
1193
- const TOLGEE_HIGHLIGHTER_CLASS = '_tolgee-highlighter';
1194
- const TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE = 'data-tolgee-key-only';
1195
- // needs to be same as in @tolgee/ui package
1196
- const DEVTOOLS_ID = '__tolgee_dev_tools';
1424
+ const ERROR_PARAM_EMPTY = 0, ERROR_UNEXPECTED_CHAR = 1, ERROR_UNEXPECTED_END = 2;
1425
+ class FormatError extends Error {
1426
+ constructor(code, index, text) {
1427
+ let error;
1428
+ if (code === ERROR_PARAM_EMPTY) {
1429
+ error = 'Empty parameter';
1430
+ }
1431
+ else if (code === ERROR_UNEXPECTED_CHAR) {
1432
+ error = 'Unexpected character';
1433
+ }
1434
+ else {
1435
+ error = 'Unexpected end';
1436
+ }
1437
+ super(`Tolgee parser: ${error} at ${index} in "${text}"`);
1438
+ this.code = code;
1439
+ this.index = index;
1440
+ }
1441
+ }
1442
+
1443
+ function isWhitespace(ch) {
1444
+ return /\s/.test(ch);
1445
+ }
1446
+ const STATE_TEXT = 0, STATE_ESCAPE_MAYBE = 1, STATE_ESCAPE = 2, STATE_PARAM = 3, STATE_PARAM_AFTER = 4;
1447
+ const END_STATES = new Set([
1448
+ STATE_ESCAPE,
1449
+ STATE_ESCAPE_MAYBE,
1450
+ STATE_TEXT,
1451
+ ]);
1452
+ const CHAR_ESCAPE = "'";
1453
+ const ESCAPABLE = new Set(['{', '}', CHAR_ESCAPE]);
1454
+ const isAllowedInParam = (char) => {
1455
+ return /[0-9a-zA-Z_]/.test(char);
1456
+ };
1457
+ function formatParser(translation) {
1458
+ let state = STATE_TEXT;
1459
+ let text = '';
1460
+ let param = '';
1461
+ let ch = '';
1462
+ const texts = [];
1463
+ const params = [];
1464
+ let i = 0;
1465
+ function parsingError(code) {
1466
+ throw new FormatError(code, i, translation);
1467
+ }
1468
+ const addText = () => {
1469
+ texts.push(text);
1470
+ text = '';
1471
+ };
1472
+ const addParamChar = () => {
1473
+ if (!isAllowedInParam(ch)) {
1474
+ parsingError(ERROR_UNEXPECTED_CHAR);
1475
+ }
1476
+ param += ch;
1477
+ };
1478
+ const addParam = () => {
1479
+ if (param === '') {
1480
+ parsingError(ERROR_PARAM_EMPTY);
1481
+ }
1482
+ params.push(param);
1483
+ param = '';
1484
+ };
1485
+ for (i = 0; i < translation.length; i++) {
1486
+ ch = translation[i];
1487
+ switch (state) {
1488
+ case STATE_TEXT:
1489
+ if (ch === CHAR_ESCAPE) {
1490
+ text += ch;
1491
+ state = STATE_ESCAPE_MAYBE;
1492
+ }
1493
+ else if (ch === '{') {
1494
+ addText();
1495
+ state = STATE_PARAM;
1496
+ }
1497
+ else {
1498
+ text += ch;
1499
+ state = STATE_TEXT;
1500
+ }
1501
+ break;
1502
+ case STATE_ESCAPE_MAYBE:
1503
+ if (ESCAPABLE.has(ch)) {
1504
+ text = text.slice(0, -1) + ch;
1505
+ state = STATE_ESCAPE;
1506
+ }
1507
+ else {
1508
+ text += ch;
1509
+ state = STATE_TEXT;
1510
+ }
1511
+ break;
1512
+ case STATE_ESCAPE:
1513
+ if (ch === CHAR_ESCAPE) {
1514
+ state = STATE_TEXT;
1515
+ }
1516
+ else {
1517
+ text += ch;
1518
+ state = STATE_ESCAPE;
1519
+ }
1520
+ break;
1521
+ case STATE_PARAM:
1522
+ if (ch === '}') {
1523
+ addParam();
1524
+ state = STATE_TEXT;
1525
+ }
1526
+ else if (!isWhitespace(ch)) {
1527
+ addParamChar();
1528
+ state = STATE_PARAM;
1529
+ }
1530
+ else if (param !== '') {
1531
+ addParam();
1532
+ state = STATE_PARAM_AFTER;
1533
+ }
1534
+ break;
1535
+ case STATE_PARAM_AFTER:
1536
+ if (ch == '}') {
1537
+ state = STATE_TEXT;
1538
+ }
1539
+ else if (isWhitespace(ch)) {
1540
+ state = STATE_PARAM_AFTER;
1541
+ }
1542
+ else {
1543
+ parsingError(ERROR_UNEXPECTED_CHAR);
1544
+ }
1545
+ }
1546
+ }
1547
+ if (!END_STATES.has(state)) {
1548
+ parsingError(ERROR_UNEXPECTED_END);
1549
+ }
1550
+ addText();
1551
+ return [texts, params];
1552
+ }
1553
+
1554
+ function formatter(translation, params) {
1555
+ const [texts, pars] = formatParser(translation);
1556
+ const result = [texts[0]];
1557
+ for (let i = 1; i < texts.length; i++) {
1558
+ const parameter = params === null || params === void 0 ? void 0 : params[pars[i - 1]];
1559
+ if (parameter === undefined) {
1560
+ throw new Error(`Missing parameter "${pars[i - 1]}" in "${translation}"`);
1561
+ }
1562
+ result.push(String(parameter));
1563
+ result.push(texts[i]);
1564
+ }
1565
+ return result.join('');
1566
+ }
1567
+
1568
+ function createFormatSimple() {
1569
+ return {
1570
+ format: ({ translation, params }) => formatter(translation, params),
1571
+ };
1572
+ }
1573
+ const FormatSimple = () => (tolgee, tools) => {
1574
+ tools.setFinalFormatter(createFormatSimple());
1575
+ return tolgee;
1576
+ };
1197
1577
 
1198
- exports.DEVTOOLS_ID = DEVTOOLS_ID;
1199
- exports.RESTRICTED_ASCENDANT_ATTRIBUTE = RESTRICTED_ASCENDANT_ATTRIBUTE;
1200
- exports.TOLGEE_ATTRIBUTE_NAME = TOLGEE_ATTRIBUTE_NAME;
1201
- exports.TOLGEE_HIGHLIGHTER_CLASS = TOLGEE_HIGHLIGHTER_CLASS;
1202
- exports.TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE = TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE;
1203
- exports.Tolgee = Tolgee;
1578
+ exports.FormatSimple = FormatSimple;
1579
+ exports.TolgeeCore = TolgeeCore;
1204
1580
  exports.getFallback = getFallback;
1205
1581
  exports.getFallbackArray = getFallbackArray;
1206
- exports.getTranslateParams = getTranslateParams;
1582
+ exports.getTranslateProps = getTranslateProps;
1207
1583
 
1208
1584
  Object.defineProperty(exports, '__esModule', { value: true });
1209
1585