@transifex/native 4.0.0 → 4.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transifex/native",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "i18n framework using Transifex Native",
5
5
  "keywords": [
6
6
  "transifex",
package/src/TxNative.js CHANGED
@@ -7,7 +7,7 @@ import SourceErrorPolicy from './policies/SourceErrorPolicy';
7
7
  import SourceStringPolicy from './policies/SourceStringPolicy';
8
8
  import MessageFormatRenderer from './renderers/MessageFormatRenderer';
9
9
  import {
10
- generateKey, isString, isPluralized, escape, generateHashedKey, sleep,
10
+ generateKey, isString, escape, generateHashedKey, sleep,
11
11
  } from './utils';
12
12
  import {
13
13
  sendEvent,
@@ -15,6 +15,7 @@ import {
15
15
  FETCHING_LOCALES, LOCALES_FETCHED, LOCALES_FETCH_FAILED,
16
16
  LOCALE_CHANGED,
17
17
  } from './events';
18
+ import { isPluralized } from './plurals';
18
19
 
19
20
  /**
20
21
  * Native instance, combines functionality from
@@ -29,6 +30,8 @@ export default class TxNative {
29
30
  this.token = '';
30
31
  this.secret = '';
31
32
  this.filterTags = '';
33
+ this.fetchTimeout = 0;
34
+ this.fetchInterval = 250;
32
35
  this.cache = new MemoryCache();
33
36
  this.missingPolicy = new SourceStringPolicy();
34
37
  this.errorPolicy = new SourceErrorPolicy();
@@ -47,6 +50,8 @@ export default class TxNative {
47
50
  * @param {String} params.filterTags
48
51
  * @param {String} params.token
49
52
  * @param {String} params.secret
53
+ * @param {Number} params.fetchTimeout
54
+ * @param {Number} params.fetchInterval
50
55
  * @param {Function} params.cache
51
56
  * @param {Function} params.missingPolicy
52
57
  * @param {Function} params.errorPolicy
@@ -60,6 +65,8 @@ export default class TxNative {
60
65
  'secret',
61
66
  'cache',
62
67
  'filterTags',
68
+ 'fetchTimeout',
69
+ 'fetchInterval',
63
70
  'missingPolicy',
64
71
  'errorPolicy',
65
72
  'stringRenderer',
@@ -182,11 +189,17 @@ export default class TxNative {
182
189
  }
183
190
  }
184
191
 
192
+ const handleError = (err) => {
193
+ sendEvent(TRANSLATIONS_FETCH_FAILED, { localeCode, filterTags }, this);
194
+ return err;
195
+ };
196
+
185
197
  // contact CDS
186
198
  try {
187
199
  sendEvent(FETCHING_TRANSLATIONS, { localeCode, filterTags }, this);
188
200
  let response;
189
201
  let lastResponseStatus = 202;
202
+ const tsNow = Date.now();
190
203
  while (lastResponseStatus === 202) {
191
204
  /* eslint-disable no-await-in-loop */
192
205
  let url = `${this.cdsHost}/content/${localeCode}`;
@@ -200,8 +213,14 @@ export default class TxNative {
200
213
  'X-NATIVE-SDK': `txjs/${__PLATFORM__}/${__VERSION__}`,
201
214
  },
202
215
  });
203
- /* eslint-enable no-await-in-loop */
204
216
  lastResponseStatus = response.status;
217
+ if (this.fetchTimeout > 0 && (Date.now() - tsNow) >= this.fetchTimeout) {
218
+ throw handleError(new Error('Fetch translations timeout'));
219
+ }
220
+ if (lastResponseStatus === 202 && this.fetchInterval > 0) {
221
+ await sleep(this.fetchInterval);
222
+ }
223
+ /* eslint-enable no-await-in-loop */
205
224
  }
206
225
 
207
226
  const { data } = response;
@@ -215,12 +234,10 @@ export default class TxNative {
215
234
  this.cache.update(localeCode, hashmap);
216
235
  sendEvent(TRANSLATIONS_FETCHED, { localeCode, filterTags }, this);
217
236
  } else {
218
- sendEvent(TRANSLATIONS_FETCH_FAILED, { localeCode, filterTags }, this);
219
- throw new Error('Could not fetch translations');
237
+ throw handleError(new Error('Could not fetch translations'));
220
238
  }
221
239
  } catch (err) {
222
- sendEvent(TRANSLATIONS_FETCH_FAILED, { localeCode, filterTags }, this);
223
- throw err;
240
+ throw handleError(err);
224
241
  }
225
242
  }
226
243
 
@@ -354,11 +371,17 @@ export default class TxNative {
354
371
 
355
372
  if (!this.token) return [];
356
373
 
374
+ const handleError = (err) => {
375
+ sendEvent(LOCALES_FETCH_FAILED, null, this);
376
+ return err;
377
+ };
378
+
357
379
  // contact CDS
358
380
  try {
359
381
  sendEvent(FETCHING_LOCALES, null, this);
360
382
  let response;
361
383
  let lastResponseStatus = 202;
384
+ const tsNow = Date.now();
362
385
  while (lastResponseStatus === 202) {
363
386
  /* eslint-disable no-await-in-loop */
364
387
  response = await axios.get(`${this.cdsHost}/languages`, {
@@ -368,8 +391,14 @@ export default class TxNative {
368
391
  'X-NATIVE-SDK': `txjs/${__PLATFORM__}/${__VERSION__}`,
369
392
  },
370
393
  });
371
- /* eslint-enable no-await-in-loop */
372
394
  lastResponseStatus = response.status;
395
+ if (this.fetchTimeout > 0 && (Date.now() - tsNow) >= this.fetchTimeout) {
396
+ throw handleError(new Error('Get locales timeout'));
397
+ }
398
+ if (lastResponseStatus === 202 && this.fetchInterval > 0) {
399
+ await sleep(this.fetchInterval);
400
+ }
401
+ /* eslint-enable no-await-in-loop */
373
402
  }
374
403
 
375
404
  const { data } = response;
@@ -378,12 +407,10 @@ export default class TxNative {
378
407
  this.locales = this.languages.map((entry) => entry.code);
379
408
  sendEvent(LOCALES_FETCHED, null, this);
380
409
  } else {
381
- sendEvent(LOCALES_FETCH_FAILED, null, this);
382
- throw new Error('Could not fetch languages');
410
+ throw handleError(new Error('Could not fetch languages'));
383
411
  }
384
412
  } catch (err) {
385
- sendEvent(LOCALES_FETCH_FAILED, null, this);
386
- throw err;
413
+ throw handleError(err);
387
414
  }
388
415
 
389
416
  return [...this.locales];
package/src/index.d.ts CHANGED
@@ -26,5 +26,7 @@ declare module '@transifex/native' {
26
26
  export const generateHashedKey: any;
27
27
  export const generateKey: any;
28
28
  export const isPluralized: any;
29
+ export const explodePlurals: any;
30
+ export const implodePlurals: any;
29
31
  export const normalizeLocale: any;
30
32
  }
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ function _createNativeInstance(initOptions) {
17
17
  }
18
18
 
19
19
  export * from './utils';
20
+ export * from './plurals';
20
21
  export * from './events';
21
22
 
22
23
  export const PseudoTranslationPolicy = _PseudoTranslationPolicy;
package/src/plurals.js ADDED
@@ -0,0 +1,204 @@
1
+ const PLURAL_RULES = ['zero', 'one', 'two', 'few', 'many', 'other'];
2
+
3
+ // The `_consumeFOO` functions take an input, "consume" a part of it to
4
+ // produce the first return value and return the "unconsumed" part of the input
5
+ // as the second return value
6
+
7
+ // '{cnt, plural, one {ONE} other {OTHER}}' =>
8
+ // ['cnt', 'one {ONE} other {OTHER}']
9
+ function consumePreamble(string) {
10
+ if (!(string.length > 1
11
+ && string[0] === '{'
12
+ && string[string.length - 1] === '}')
13
+ ) {
14
+ return [null, null];
15
+ }
16
+
17
+ // {cnt, plural, one {foo} other {foos}}
18
+ // ^^^^^^^^^^^
19
+ const firstCommaPos = string.indexOf(',');
20
+ if (firstCommaPos === -1) { return [null, null]; }
21
+ const secondCommaPos = string.indexOf(',', firstCommaPos + 1);
22
+ if (secondCommaPos === -1) { return [null, null]; }
23
+
24
+ const varName = string.substring(1, firstCommaPos).trim();
25
+ const keyword = string
26
+ .substring(firstCommaPos + 1, secondCommaPos)
27
+ .trim();
28
+
29
+ // Make sure `varName` is a proper variable and that `keyword` is 'plural'
30
+ if (!/^[\w_]+$/.test(varName) || keyword !== 'plural') {
31
+ return [null, null];
32
+ }
33
+
34
+ return [
35
+ varName,
36
+ string.substring(secondCommaPos + 1, string.length - 1).trim()];
37
+ }
38
+
39
+ // 'one {ONE} other {OTHER}' => ['one', '{ONE} other {OTHER}']
40
+ // 'other {OTHER}' => ['other', '{OTHER}']
41
+ function consumeRule(string) {
42
+ // {cnt, plural, one {foo} other {foos}}
43
+ // ^^^^^
44
+ const leftBracketPos = string.indexOf('{');
45
+ if (leftBracketPos === -1) { return [null, null]; }
46
+ let rule = string.substring(0, leftBracketPos).trim();
47
+ if (rule[0] === '=') {
48
+ rule = rule.substring(1);
49
+ if (!Number.isNaN(parseInt(rule, 10)) && parseInt(rule, 10) === parseFloat(rule)) {
50
+ rule = parseInt(rule, 10);
51
+ rule = PLURAL_RULES[rule];
52
+ if (rule === undefined) { return [null, null]; }
53
+ } else { return [null, null]; }
54
+ } else if (PLURAL_RULES.indexOf(rule) === -1) {
55
+ return [null, null];
56
+ }
57
+ return [rule, string.substring(leftBracketPos)];
58
+ }
59
+
60
+ // '{ONE} other {OTHER}' => ['ONE', 'other {OTHER}']
61
+ // '{OTHER}' => ['OTHER', '']
62
+ function consumePlural(string) {
63
+ // {cnt, plural, one {foo} other {foos}}
64
+ // ^^^^^
65
+ let [bracketCount, escaping] = [0, false];
66
+ let ptr = 0;
67
+ while (ptr < string.length) {
68
+ const char = string[ptr];
69
+ if (char === "'") {
70
+ const peek = string[ptr + 1];
71
+ if (peek === "'") {
72
+ ptr += 1;
73
+ } else if (escaping) {
74
+ escaping = false;
75
+ } else if (peek === '{' || peek === '}') {
76
+ escaping = true;
77
+ }
78
+ } else if (char === '{') {
79
+ if (!escaping) {
80
+ bracketCount += 1;
81
+ }
82
+ } else if (char === '}') {
83
+ if (!escaping) {
84
+ bracketCount -= 1;
85
+ }
86
+ if (bracketCount === 0) {
87
+ return [string.substring(1, ptr), string.substring(ptr + 1).trim()];
88
+ }
89
+ }
90
+ ptr += 1;
91
+ }
92
+ return [null, null];
93
+ }
94
+
95
+ /* Make an effort to split (explode) an ICU message format string into
96
+ * plurals. Works on only the simplest case, when a gettext-like statement
97
+ * like:
98
+ *
99
+ * ngettext("You have %d new message", "You have %d new messages", cnt)
100
+ *
101
+ * was converted into an ICU message like:
102
+ *
103
+ * {cnt, plural,
104
+ * one {You have # new message}
105
+ * other {You have # new messages}}
106
+ *
107
+ * The return value is a size-2 array with the name of the variable used and
108
+ * an object mapping the plural rule to the plural version, eg:
109
+ *
110
+ * ['cnt',
111
+ * {one: 'You have # new message',
112
+ * other: 'You have # new messages'}]
113
+ *
114
+ * If the string can't be converted for any reason, `null` will be used in
115
+ * place of the variable name and only rule=5 (other) will be returned.
116
+ *
117
+ * @function
118
+ * @param {String} string
119
+ * @returns {[String, {rule: String}]} variable name and plural forms pair
120
+ *
121
+ * */
122
+ export function explodePlurals(string) {
123
+ const defaultResult = [null, { other: string }];
124
+
125
+ // {cnt, plural, one {foo} other {foos}}
126
+ // ^^^^^^^^^^^^
127
+ // eslint-disable-next-line prefer-const
128
+ let [varName, remaining] = consumePreamble(string);
129
+ if (varName == null) { return defaultResult; }
130
+
131
+ // {cnt, plural, one {foo} other {foos}}
132
+ // ^^^^^^^^^
133
+ const plurals = {};
134
+ let rule;
135
+ let plural;
136
+ [rule, remaining] = consumeRule(remaining);
137
+ if (rule == null) { return defaultResult; }
138
+
139
+ [plural, remaining] = consumePlural(remaining.trim());
140
+ if (plural == null) { return defaultResult; }
141
+ plurals[rule] = plural;
142
+
143
+ while (remaining.trim()) {
144
+ // {cnt, plural, one {foo} other {foos}}
145
+ // ^^^^^^^^^^^^
146
+ [rule, remaining] = consumeRule(remaining.trim());
147
+ if (rule == null) { return defaultResult; }
148
+ [plural, remaining] = consumePlural(remaining.trim());
149
+ if (plural == null) { return defaultResult; }
150
+ plurals[rule] = plural;
151
+ }
152
+
153
+ if ((Object.keys(plurals).length === 1 && !('other' in plurals))
154
+ || (!('one' in plurals && 'other' in plurals))
155
+ ) {
156
+ return defaultResult;
157
+ }
158
+
159
+ return [varName, plurals];
160
+ }
161
+
162
+ /* Convert a plurals object into an ICU plural statement, eg:
163
+ *
164
+ * {one: "ONE", other: "OTHER"} => '{???, plural, one {ONE} other {OTHER}}'
165
+ *
166
+ * @function
167
+ * @param {Object} plurals
168
+ * @param {string} [count='???']
169
+ * @returns {String}
170
+ */
171
+ export function implodePlurals(plurals, count = '???') {
172
+ const result = [`{${count}, plural,`];
173
+ // Lets get the rules in order
174
+ // eslint-disable-next-line no-restricted-syntax
175
+ for (const rule of PLURAL_RULES) {
176
+ if (rule in plurals) {
177
+ const plural = plurals[rule];
178
+ result.push(` ${rule} {${plural}}`);
179
+ }
180
+ }
181
+ result.push('}');
182
+ return result.join('');
183
+ }
184
+
185
+ /**
186
+ * Determine if a string is an ICU pluralized string. Only works for the
187
+ * simplest possible case, where the string starts and ends in '{' and '}'
188
+ * respectively and the root ICU directive is a plural statement. ie This
189
+ *
190
+ * {cnt, plural, one {ONE} other {OTHER}}
191
+ *
192
+ * will return 'true' while this
193
+ *
194
+ * hello {cnt, plural, one {ONE} other {OTHER}}
195
+ *
196
+ * will return 'false'.
197
+ *
198
+ * @param {String} string
199
+ * @returns {Boolean}
200
+ */
201
+ export function isPluralized(string) {
202
+ const plurals = explodePlurals(string)[1];
203
+ return Object.keys(plurals).length > 1;
204
+ }
package/src/utils.js CHANGED
@@ -84,138 +84,6 @@ export function normalizeLocale(locale) {
84
84
  return normalizedLocale;
85
85
  }
86
86
 
87
- const PLURAL_RULES = ['zero', 'one', 'two', 'few', 'many', 'other'];
88
-
89
- function _consumePreamble(string) {
90
- if (!(string.length > 1
91
- && string[0] === '{'
92
- && string[string.length - 1] === '}')
93
- ) {
94
- return [null, null];
95
- }
96
-
97
- // {cnt, plural, one {foo} other {foos}}
98
- // ^^^^^^^^^^^
99
- const firstCommaPos = string.indexOf(',');
100
- if (firstCommaPos === -1) { return [null, null]; }
101
- const secondCommaPos = string.indexOf(',', firstCommaPos + 1);
102
- if (secondCommaPos === -1) { return [null, null]; }
103
-
104
- const varName = string.substring(1, firstCommaPos).trim();
105
- const keyword = string
106
- .substring(firstCommaPos + 1, secondCommaPos)
107
- .trim();
108
-
109
- // Make sure `keyword` is 'plural'
110
- if (keyword !== 'plural') { return [null, null]; }
111
-
112
- return [
113
- varName,
114
- string.substring(secondCommaPos + 1, string.length - 1).trim()];
115
- }
116
-
117
- function _consumeRule(string) {
118
- // {cnt, plural, one {foo} other {foos}}
119
- // ^^^^^
120
- const leftBracketPos = string.indexOf('{');
121
- if (leftBracketPos === -1) { return [null, null]; }
122
- let rule = string.substring(0, leftBracketPos).trim();
123
- if (rule[0] === '=') {
124
- rule = rule.substring(1);
125
- if (!Number.isNaN(parseInt(rule, 10)) && parseInt(rule, 10) === parseFloat(rule)) {
126
- rule = parseInt(rule, 10);
127
- rule = PLURAL_RULES[rule];
128
- if (rule === undefined) { return [null, null]; }
129
- } else { return [null, null]; }
130
- } else if (PLURAL_RULES.indexOf(rule) === -1) {
131
- return [null, null];
132
- }
133
- return [rule, string.substring(leftBracketPos)];
134
- }
135
-
136
- function _consumePlural(string) {
137
- // {cnt, plural, one {foo} other {foos}}
138
- // ^^^^^
139
- let [bracketCount, escaping] = [0, false];
140
- let ptr = 0;
141
- while (ptr < string.length) {
142
- const char = string[ptr];
143
- if (char === "'") {
144
- const peek = string[ptr + 1];
145
- if (peek === "'") {
146
- ptr += 1;
147
- } else if (escaping) {
148
- escaping = false;
149
- } else if (peek === '{' || peek === '}') {
150
- escaping = true;
151
- }
152
- } else if (char === '{') {
153
- if (!escaping) {
154
- bracketCount += 1;
155
- }
156
- } else if (char === '}') {
157
- if (!escaping) {
158
- bracketCount -= 1;
159
- }
160
- if (bracketCount === 0) {
161
- return [string.substring(1, ptr), string.substring(ptr + 1).trim()];
162
- }
163
- }
164
- ptr += 1;
165
- }
166
- return [null, null];
167
- }
168
-
169
- /**
170
- * Determine if a string is an ICU pluralized string. Only works for the
171
- * simplest possible case, where the string starts and ends in '{' and '}'
172
- * respectively and the root ICU directive is a plural statement. ie This
173
- *
174
- * {cnt, plural, one {ONE} other {OTHER}}
175
- *
176
- * will return 'true' while this
177
- *
178
- * hello {cnt, plural, one {ONE} other {OTHER}}
179
- *
180
- * will return 'false'.
181
- *
182
- * @param {String} string
183
- * @returns {Boolean}
184
- */
185
- export function isPluralized(string) {
186
- const [varName, rest] = _consumePreamble(string);
187
- let remaining = rest;
188
- if (varName == null) { return false; }
189
-
190
- const plurals = {};
191
- let rule;
192
- let plural;
193
- [rule, remaining] = _consumeRule(remaining);
194
- if (rule == null) { return false; }
195
-
196
- [plural, remaining] = _consumePlural(remaining.trim());
197
- if (plural == null) { return false; }
198
- plurals[rule] = plural;
199
-
200
- while (remaining.trim()) {
201
- // {cnt, plural, one {foo} other {foos}}
202
- // ^^^^^^^^^^^^
203
- [rule, remaining] = _consumeRule(remaining.trim());
204
- if (rule == null) { return false; }
205
- [plural, remaining] = _consumePlural(remaining.trim());
206
- if (plural == null) { return false; }
207
- plurals[rule] = plural;
208
- }
209
-
210
- if ((Object.keys(plurals).length === 1 && !('other' in plurals))
211
- || (!('one' in plurals && 'other' in plurals))
212
- ) {
213
- return false;
214
- }
215
-
216
- return true;
217
- }
218
-
219
87
  /**
220
88
  * Sleep in msec
221
89
  *