@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/README.md +8 -0
- package/dist/browser.native.js +1 -1
- package/dist/browser.native.js.LICENSE.txt +2 -0
- package/dist/browser.native.js.map +1 -1
- package/dist/node.native.d.ts +2 -0
- package/dist/node.native.js +1 -1
- package/dist/node.native.js.map +1 -1
- package/package.json +1 -1
- package/src/TxNative.js +38 -11
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -0
- package/src/plurals.js +204 -0
- package/src/utils.js +0 -132
package/package.json
CHANGED
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,
|
|
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
|
-
|
|
219
|
-
throw new Error('Could not fetch translations');
|
|
237
|
+
throw handleError(new Error('Could not fetch translations'));
|
|
220
238
|
}
|
|
221
239
|
} catch (err) {
|
|
222
|
-
|
|
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
|
-
|
|
382
|
-
throw new Error('Could not fetch languages');
|
|
410
|
+
throw handleError(new Error('Could not fetch languages'));
|
|
383
411
|
}
|
|
384
412
|
} catch (err) {
|
|
385
|
-
|
|
386
|
-
throw err;
|
|
413
|
+
throw handleError(err);
|
|
387
414
|
}
|
|
388
415
|
|
|
389
416
|
return [...this.locales];
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
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
|
*
|