@tolgee/core 5.0.0-rc.9be0f0e.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 +723 -351
  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} +722 -346
  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 +723 -351
  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 +31 -31
  40. package/src/Controller/Cache/helpers.ts +6 -6
  41. package/src/Controller/Controller.ts +78 -50
  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
@@ -1,21 +1,30 @@
1
1
  import {
2
2
  CacheDescriptor,
3
3
  CacheDescriptorInternal,
4
- EventEmitterType,
5
- FallbackNSTranslation,
4
+ DevCredentials,
5
+ NsFallback,
6
+ NsType,
6
7
  } from '../../types';
8
+
7
9
  import { decodeCacheKey } from '../Cache/helpers';
8
- import { getFallbackArray, getFallbackFromStruct, unique } from './helpers';
9
- import { initState, Options } from './initState';
10
+ import { EventEmitterInstance } from '../Events/EventEmitter';
11
+ import {
12
+ getFallbackArray,
13
+ getFallbackFromStruct,
14
+ sanitizeUrl,
15
+ unique,
16
+ } from '../../helpers';
17
+ import { initState, TolgeeOptions } from './initState';
10
18
 
11
19
  export const State = (
12
- onLanguageChange: EventEmitterType<string>,
13
- onPendingLanguageChange: EventEmitterType<string>,
14
- onRunningChange: EventEmitterType<boolean>
20
+ onLanguageChange: EventEmitterInstance<string>,
21
+ onPendingLanguageChange: EventEmitterInstance<string>,
22
+ onRunningChange: EventEmitterInstance<boolean>
15
23
  ) => {
16
24
  let state = initState();
25
+ let devCredentials: DevCredentials = undefined;
17
26
 
18
- function init(options?: Partial<Options>) {
27
+ function init(options?: Partial<TolgeeOptions>) {
19
28
  state = initState(options, state);
20
29
  }
21
30
 
@@ -42,14 +51,6 @@ export const State = (
42
51
  return state.language || state.initialOptions.language;
43
52
  }
44
53
 
45
- function getLanguageOrFail() {
46
- const language = state.language || state.initialOptions.language;
47
- if (!language) {
48
- throw new Error(`No language set`);
49
- }
50
- return language;
51
- }
52
-
53
54
  function setLanguage(language: string) {
54
55
  if (state.language !== language) {
55
56
  state.language = language;
@@ -69,10 +70,10 @@ export const State = (
69
70
  }
70
71
 
71
72
  function getInitialOptions() {
72
- return state.initialOptions;
73
+ return { ...state.initialOptions, ...devCredentials };
73
74
  }
74
75
 
75
- function addActiveNs(ns: FallbackNSTranslation) {
76
+ function addActiveNs(ns: NsFallback) {
76
77
  const namespaces = getFallbackArray(ns);
77
78
  namespaces.forEach((namespace) => {
78
79
  const value = state.activeNamespaces.get(namespace);
@@ -84,7 +85,7 @@ export const State = (
84
85
  });
85
86
  }
86
87
 
87
- function removeActiveNs(ns: FallbackNSTranslation) {
88
+ function removeActiveNs(ns: NsFallback) {
88
89
  const namespaces = getFallbackArray(ns);
89
90
  namespaces.forEach((namespace) => {
90
91
  const value = state.activeNamespaces.get(namespace);
@@ -99,6 +100,7 @@ export const State = (
99
100
  function getRequiredNamespaces() {
100
101
  return unique([
101
102
  ...(state.initialOptions.ns || [state.initialOptions.defaultNs]),
103
+ ...getFallbackArray(state.initialOptions.fallbackNs),
102
104
  ...state.activeNamespaces.keys(),
103
105
  ]);
104
106
  }
@@ -114,11 +116,12 @@ export const State = (
114
116
  ]);
115
117
  }
116
118
 
117
- function getFallbackNamespaces() {
118
- const defaultNs = state.initialOptions.defaultNs;
119
- const fallbackNs = state.initialOptions.fallbackNs;
120
- const fallbackNamespaces = typeof defaultNs === 'string' ? [defaultNs] : [];
121
- return unique([...fallbackNamespaces, ...getFallbackArray(fallbackNs)]);
119
+ function getFallbackNs() {
120
+ return getFallbackArray(state.initialOptions.fallbackNs);
121
+ }
122
+
123
+ function getDefaultNs(ns?: NsType) {
124
+ return ns === undefined ? state.initialOptions.defaultNs : ns;
122
125
  }
123
126
 
124
127
  function getAvailableLanguages() {
@@ -142,6 +145,17 @@ export const State = (
142
145
  };
143
146
  }
144
147
 
148
+ function overrideCredentials(credentials: DevCredentials) {
149
+ if (credentials) {
150
+ devCredentials = {
151
+ ...credentials,
152
+ apiUrl: sanitizeUrl(credentials.apiUrl),
153
+ };
154
+ } else {
155
+ devCredentials = undefined;
156
+ }
157
+ }
158
+
145
159
  return Object.freeze({
146
160
  init,
147
161
  isRunning,
@@ -149,7 +163,6 @@ export const State = (
149
163
  isInitialLoading,
150
164
  setInitialLoading,
151
165
  getLanguage,
152
- getLanguageOrFail,
153
166
  setLanguage,
154
167
  getPendingLanguage,
155
168
  setPendingLanguage,
@@ -158,8 +171,12 @@ export const State = (
158
171
  removeActiveNs,
159
172
  getRequiredNamespaces,
160
173
  getFallbackLangs,
161
- getFallbackNamespaces,
174
+ getFallbackNs,
175
+ getDefaultNs,
162
176
  getAvailableLanguages,
163
177
  withDefaultNs,
178
+ overrideCredentials,
164
179
  });
165
180
  };
181
+
182
+ export type StateInstance = ReturnType<typeof State>;
@@ -1,57 +1,111 @@
1
1
  import {
2
+ FallbackGeneral,
2
3
  FallbackLanguageOption,
3
- FallbackNS,
4
4
  TreeTranslationsData,
5
+ OnFormatError,
5
6
  } from '../../types';
7
+ import { sanitizeUrl } from '../../helpers';
8
+ import {
9
+ defaultObserverOptions,
10
+ ObserverOptions,
11
+ ObserverOptionsInternal,
12
+ } from './observerOptions';
13
+
14
+ export const DEFAULT_FORMAT_ERROR = 'invalid';
6
15
 
7
- export type Options = {
16
+ export type TolgeeStaticData = {
17
+ [key: string]: TreeTranslationsData | (() => Promise<TreeTranslationsData>);
18
+ };
19
+
20
+ export type TolgeeOptionsInternal = {
8
21
  /**
9
22
  * Initial language
10
23
  */
11
24
  language?: string;
25
+
26
+ /**
27
+ * Tolgee instance url (e.g. https://app.tolgee.io)
28
+ */
12
29
  apiUrl?: string;
30
+
31
+ /**
32
+ * Project API key (PAK) or Personal Access Token (PAT)
33
+ */
13
34
  apiKey?: string;
14
- projectId?: number;
35
+
36
+ /**
37
+ * Project id is necessary if you are using PAT
38
+ */
39
+ projectId?: number | string;
40
+
15
41
  /**
16
42
  * Used when auto detection is not available or is turned off
17
43
  */
18
44
  defaultLanguage?: string;
45
+
19
46
  /**
20
47
  * Languages which can be used for language detection
21
48
  * and also limits which values can be stored
22
49
  */
23
50
  availableLanguages?: string[];
51
+
24
52
  /**
25
53
  * Language which is used when no translation is available for current one
26
54
  */
27
55
  fallbackLanguage?: FallbackLanguageOption;
28
- /**
29
- * Store user language in localStorage (default: true)
30
- */
31
- enableLanguageStore?: boolean;
56
+
32
57
  /**
33
58
  * Namespaces which should be always fetched
34
59
  */
35
60
  ns?: string[];
61
+
36
62
  /**
37
63
  * Namespaces to be used to find translation when no explicit namespace set.
38
64
  */
39
- fallbackNs?: FallbackNS;
65
+ fallbackNs?: FallbackGeneral;
66
+
40
67
  /**
41
68
  * Default namespace when no namespace defined (default: '')
42
69
  */
43
70
  defaultNs: string;
71
+
44
72
  /**
45
- * Prefix used for fetching languages (default: 'i18n/')
73
+ * These data go directly to cache or you can specify async
74
+ * function which will be used to get the data. Use `:` to add namespace:
75
+ *
76
+ * ```ts
77
+ * {
78
+ * 'locale': <translations | async function>
79
+ * 'locale:namespace': <translations | async function>
80
+ * }
81
+ * ```
46
82
  */
47
- filesUrlPrefix: string;
48
- staticData?: {
49
- [key: string]: TreeTranslationsData | (() => Promise<TreeTranslationsData>);
50
- };
83
+ staticData?: TolgeeStaticData;
84
+
85
+ /**
86
+ * Switches between invisible and text observer. (Default: invisible)
87
+ */
88
+ observerType: 'invisible' | 'text';
89
+
90
+ /**
91
+ * Observer options object.
92
+ */
93
+ observerOptions: ObserverOptionsInternal;
94
+
95
+ /**
96
+ * Define what to display in case of formatting error. (Default: 'invalid')
97
+ */
98
+ onFormatError: OnFormatError;
99
+ };
100
+
101
+ export type TolgeeOptions = Partial<
102
+ Omit<TolgeeOptionsInternal, 'observerOptions'>
103
+ > & {
104
+ observerOptions?: ObserverOptions;
51
105
  };
52
106
 
53
107
  export type State = {
54
- initialOptions: Options;
108
+ initialOptions: TolgeeOptionsInternal;
55
109
  activeNamespaces: Map<string, number>;
56
110
  language: string | undefined;
57
111
  pendingLanguage: string | undefined;
@@ -59,24 +113,42 @@ export type State = {
59
113
  isRunning: boolean;
60
114
  };
61
115
 
62
- const defaultValues: Options = {
63
- enableLanguageStore: true,
116
+ const defaultValues: TolgeeOptionsInternal = {
64
117
  defaultNs: '',
65
- filesUrlPrefix: 'i18n/',
118
+ observerOptions: defaultObserverOptions,
119
+ observerType: 'invisible',
120
+ onFormatError: DEFAULT_FORMAT_ERROR,
121
+ };
122
+
123
+ export const combineOptions = <T extends TolgeeOptions>(
124
+ ...states: (T | undefined)[]
125
+ ) => {
126
+ let result = {} as T;
127
+ states.forEach((state) => {
128
+ result = {
129
+ ...result,
130
+ ...state,
131
+ observerOptions: {
132
+ ...result.observerOptions,
133
+ ...state?.observerOptions,
134
+ },
135
+ };
136
+ });
137
+ return result;
66
138
  };
67
139
 
68
140
  export const initState = (
69
- options?: Partial<Options>,
141
+ options?: Partial<TolgeeOptions>,
70
142
  previousState?: State
71
143
  ): State => {
72
- const initialOptions = {
73
- ...defaultValues,
74
- ...previousState?.initialOptions,
75
- ...options,
76
- };
144
+ const initialOptions = combineOptions(
145
+ defaultValues,
146
+ previousState?.initialOptions,
147
+ options
148
+ ) as TolgeeOptionsInternal;
149
+
77
150
  // remove extra '/' from url end
78
- const apiUrl = initialOptions.apiUrl;
79
- initialOptions.apiUrl = apiUrl ? apiUrl.replace(/\/+$/, '') : apiUrl;
151
+ initialOptions.apiUrl = sanitizeUrl(initialOptions.apiUrl);
80
152
 
81
153
  return {
82
154
  initialOptions,
@@ -0,0 +1,66 @@
1
+ export type ObserverOptionsInternal = {
2
+ /**
3
+ * Attributes that are observed on html elements
4
+ */
5
+ tagAttributes: Record<string, string[]>;
6
+
7
+ /**
8
+ * Key(s) which trigger in-context translating (default: ['Alt'])
9
+ */
10
+ highlightKeys: ModifierKey[];
11
+
12
+ /**
13
+ * Color used for key highlighting (default: 'rgb(255, 0, 0)')
14
+ */
15
+ highlightColor: string;
16
+
17
+ /**
18
+ * Highliter border width (default: 5)
19
+ */
20
+ highlightWidth: number;
21
+
22
+ /**
23
+ * Root element which will be observed (default: document.body)
24
+ */
25
+ targetElement?: HTMLElement;
26
+
27
+ /**
28
+ * Elements which are not observed (default: ['script', 'style'])
29
+ */
30
+ restrictedElements: string[];
31
+
32
+ /**
33
+ * Text observer prefix (default: '%-%tolgee:')
34
+ */
35
+ inputPrefix: string;
36
+
37
+ /**
38
+ * Text observer suffix (default: '%-%')
39
+ */
40
+ inputSuffix: string;
41
+
42
+ /**
43
+ * Html elements which will pass click listener to their parent (default: ['option', 'optgroup'])
44
+ */
45
+ passToParent: (keyof HTMLElementTagNameMap)[] | ((node: Element) => boolean);
46
+ };
47
+
48
+ export type ObserverOptions = Partial<ObserverOptionsInternal>;
49
+
50
+ export type ModifierKey = 'Alt' | 'Control' | 'Shift' | 'Meta';
51
+
52
+ export const defaultObserverOptions: ObserverOptionsInternal = {
53
+ tagAttributes: {
54
+ textarea: ['placeholder'],
55
+ input: ['value', 'placeholder'],
56
+ img: ['alt'],
57
+ '*': ['aria-label', 'title'],
58
+ },
59
+ restrictedElements: ['script', 'style'],
60
+ highlightKeys: ['Alt'] as ModifierKey[],
61
+ highlightColor: 'rgb(255, 0, 0)',
62
+ highlightWidth: 5,
63
+ inputPrefix: '%-%tolgee:',
64
+ inputSuffix: '%-%',
65
+ passToParent: ['option', 'optgroup'],
66
+ };
@@ -2,7 +2,7 @@ export const ValueObserver = <T = any>(
2
2
  initialValue: T,
3
3
  valueGetter: () => T,
4
4
  handler: (value: T) => void
5
- ) => {
5
+ ): ValueObserverInstance<T> => {
6
6
  let previousValue: T = initialValue;
7
7
  function init(value: T) {
8
8
  previousValue = value;
@@ -20,4 +20,7 @@ export const ValueObserver = <T = any>(
20
20
  });
21
21
  };
22
22
 
23
- export type ValueObserverInstance<T> = ReturnType<typeof ValueObserver<T>>;
23
+ export type ValueObserverInstance<T> = {
24
+ readonly init: (value: T) => void;
25
+ readonly notify: () => void;
26
+ };
@@ -0,0 +1,26 @@
1
+ export const ERROR_PARAM_EMPTY = 0,
2
+ ERROR_UNEXPECTED_CHAR = 1,
3
+ ERROR_UNEXPECTED_END = 2;
4
+
5
+ export type ErrorCode =
6
+ | typeof ERROR_PARAM_EMPTY
7
+ | typeof ERROR_UNEXPECTED_CHAR
8
+ | typeof ERROR_UNEXPECTED_END;
9
+
10
+ export class FormatError extends Error {
11
+ public readonly code: ErrorCode;
12
+ public readonly index: number;
13
+ constructor(code: ErrorCode, index: number, text: string) {
14
+ let error: string;
15
+ if (code === ERROR_PARAM_EMPTY) {
16
+ error = 'Empty parameter';
17
+ } else if (code === ERROR_UNEXPECTED_CHAR) {
18
+ error = 'Unexpected character';
19
+ } else {
20
+ error = 'Unexpected end';
21
+ }
22
+ super(`Tolgee parser: ${error} at ${index} in "${text}"`);
23
+ this.code = code;
24
+ this.index = index;
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ import { formatter } from './formatter';
2
+ import { FinalFormatterMiddleware, TolgeePlugin } from '../types';
3
+
4
+ function createFormatSimple(): FinalFormatterMiddleware {
5
+ return {
6
+ format: ({ translation, params }) => formatter(translation, params),
7
+ };
8
+ }
9
+
10
+ export const FormatSimple = (): TolgeePlugin => (tolgee, tools) => {
11
+ tools.setFinalFormatter(createFormatSimple());
12
+ return tolgee;
13
+ };
@@ -0,0 +1,133 @@
1
+ import {
2
+ ErrorCode,
3
+ ERROR_PARAM_EMPTY,
4
+ ERROR_UNEXPECTED_CHAR,
5
+ ERROR_UNEXPECTED_END,
6
+ FormatError,
7
+ } from './FormatError';
8
+
9
+ function isWhitespace(ch: string) {
10
+ return /\s/.test(ch);
11
+ }
12
+
13
+ const STATE_TEXT = 0,
14
+ STATE_ESCAPE_MAYBE = 1,
15
+ STATE_ESCAPE = 2,
16
+ STATE_PARAM = 3,
17
+ STATE_PARAM_AFTER = 4;
18
+
19
+ type State =
20
+ | typeof STATE_TEXT
21
+ | typeof STATE_ESCAPE_MAYBE
22
+ | typeof STATE_ESCAPE
23
+ | typeof STATE_PARAM
24
+ | typeof STATE_PARAM_AFTER;
25
+
26
+ const END_STATES = new Set<State>([
27
+ STATE_ESCAPE,
28
+ STATE_ESCAPE_MAYBE,
29
+ STATE_TEXT,
30
+ ]);
31
+ const CHAR_ESCAPE = "'";
32
+ const ESCAPABLE = new Set(['{', '}', CHAR_ESCAPE]);
33
+
34
+ const isAllowedInParam = (char: string) => {
35
+ return /[0-9a-zA-Z_]/.test(char);
36
+ };
37
+
38
+ export function formatParser(translation: string) {
39
+ let state: State = STATE_TEXT;
40
+ let text = '';
41
+ let param = '';
42
+ let ch = '';
43
+ const texts: string[] = [];
44
+ const params: string[] = [];
45
+
46
+ let i = 0;
47
+
48
+ function parsingError(code: ErrorCode): never {
49
+ throw new FormatError(code, i, translation);
50
+ }
51
+
52
+ const addText = () => {
53
+ texts.push(text);
54
+ text = '';
55
+ };
56
+
57
+ const addParamChar = () => {
58
+ if (!isAllowedInParam(ch)) {
59
+ parsingError(ERROR_UNEXPECTED_CHAR);
60
+ }
61
+ param += ch;
62
+ };
63
+
64
+ const addParam = () => {
65
+ if (param === '') {
66
+ parsingError(ERROR_PARAM_EMPTY);
67
+ }
68
+ params.push(param);
69
+ param = '';
70
+ };
71
+
72
+ for (i = 0; i < translation.length; i++) {
73
+ ch = translation[i];
74
+ switch (state) {
75
+ case STATE_TEXT:
76
+ if (ch === CHAR_ESCAPE) {
77
+ text += ch;
78
+ state = STATE_ESCAPE_MAYBE;
79
+ } else if (ch === '{') {
80
+ addText();
81
+ state = STATE_PARAM;
82
+ } else {
83
+ text += ch;
84
+ state = STATE_TEXT;
85
+ }
86
+ break;
87
+
88
+ case STATE_ESCAPE_MAYBE:
89
+ if (ESCAPABLE.has(ch)) {
90
+ text = text.slice(0, -1) + ch;
91
+ state = STATE_ESCAPE;
92
+ } else {
93
+ text += ch;
94
+ state = STATE_TEXT;
95
+ }
96
+ break;
97
+ case STATE_ESCAPE:
98
+ if (ch === CHAR_ESCAPE) {
99
+ state = STATE_TEXT;
100
+ } else {
101
+ text += ch;
102
+ state = STATE_ESCAPE;
103
+ }
104
+ break;
105
+ case STATE_PARAM:
106
+ if (ch === '}') {
107
+ addParam();
108
+ state = STATE_TEXT;
109
+ } else if (!isWhitespace(ch)) {
110
+ addParamChar();
111
+ state = STATE_PARAM;
112
+ } else if (param !== '') {
113
+ addParam();
114
+ state = STATE_PARAM_AFTER;
115
+ }
116
+
117
+ break;
118
+ case STATE_PARAM_AFTER:
119
+ if (ch == '}') {
120
+ state = STATE_TEXT;
121
+ } else if (isWhitespace(ch)) {
122
+ state = STATE_PARAM_AFTER;
123
+ } else {
124
+ parsingError(ERROR_UNEXPECTED_CHAR);
125
+ }
126
+ }
127
+ }
128
+ if (!END_STATES.has(state)) {
129
+ parsingError(ERROR_UNEXPECTED_END);
130
+ }
131
+ addText();
132
+ return [texts, params];
133
+ }