@esgettext/runtime 1.1.0 → 1.3.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.
@@ -1,6 +1,14 @@
1
1
  import { readFile } from 'fs';
2
2
 
3
3
  let userLocalesSelected = ['C'];
4
+ /*
5
+ * Force an execution environment. By default, the environment (NodeJS or
6
+ * browser) is auto-detected. You can force the library to assume a certain
7
+ * environment with this function.
8
+ *
9
+ * @param browser - whether to assume a browser or not
10
+ * @returns the new setting.
11
+ */
4
12
  function userLocales(locales) {
5
13
  if (typeof locales !== 'undefined') {
6
14
  userLocalesSelected = locales;
@@ -8,6 +16,39 @@ function userLocales(locales) {
8
16
  return userLocalesSelected;
9
17
  }
10
18
 
19
+ /******************************************************************************
20
+ Copyright (c) Microsoft Corporation.
21
+
22
+ Permission to use, copy, modify, and/or distribute this software for any
23
+ purpose with or without fee is hereby granted.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
26
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
27
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
28
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
29
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
30
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
31
+ PERFORMANCE OF THIS SOFTWARE.
32
+ ***************************************************************************** */
33
+ /* global Reflect, Promise, SuppressedError, Symbol */
34
+
35
+
36
+ function __awaiter(thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ }
45
+
46
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
47
+ var e = new Error(message);
48
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
49
+ };
50
+
51
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
11
52
  class TransportHttp {
12
53
  loadFile(url) {
13
54
  return new Promise((resolve, reject) => {
@@ -38,23 +79,60 @@ class TransportFs {
38
79
  }
39
80
  }
40
81
 
82
+ /*
83
+ * Function for germanic plural. Returns singular (0) for 1 item, and
84
+ * 1 for everything else.
85
+ *
86
+ * @param numItems - number of items
87
+ * @returns the index into the plural translations
88
+ */
41
89
  function germanicPlural(numItems) {
42
90
  return numItems === 1 ? 0 : 1;
43
91
  }
44
92
 
93
+ /*
94
+ * A minimalistic buffer implementation that can only read 32 bit unsigned
95
+ * integers and strings.
96
+ */
45
97
  class DataViewlet {
98
+ /*
99
+ * Create a DataViewlet instance. All encodings that are supported by
100
+ * the runtime environments `TextDecoder` interface.
101
+ *
102
+ * @param array - a `Unit8Array` view on the binary buffer
103
+ * @param encoding - encoding of strings, defaults to utf-8
104
+ */
46
105
  constructor(array, encoding = 'utf-8') {
47
106
  this.array = array;
48
107
  this.decoder = new TextDecoder(encoding);
49
108
  this._encoding = encoding;
50
109
  }
110
+ /**
111
+ * Get the encoding for strings.
112
+ *
113
+ * @returns the encoding in use
114
+ */
51
115
  get encoding() {
52
116
  return this._encoding;
53
117
  }
118
+ /**
119
+ * Switch to a new encoding.
120
+ *
121
+ * @param encoding - new encoding to use
122
+ */
54
123
  set encoding(encoding) {
55
124
  this.decoder = new TextDecoder(encoding);
56
125
  this._encoding = encoding;
57
126
  }
127
+ /*
128
+ * Reads an unsigned 32-bit integer from the buffer at
129
+ * the specified offset as big-endian.
130
+ *
131
+ * @param offset - Number of bytes to skip before starting to read.
132
+ * Must satisfy `0 <= offset <= buf.length - 4`.
133
+ * Default: 0.
134
+ * @returns the 32-bit unsigned integer at position `offset`.s
135
+ */
58
136
  readUInt32BE(offset = 0) {
59
137
  if (offset + 4 > this.array.byteLength + this.array.byteOffset) {
60
138
  throw new Error('read past array end');
@@ -65,6 +143,15 @@ class DataViewlet {
65
143
  this.array[offset + 3]) >>>
66
144
  0);
67
145
  }
146
+ /*
147
+ * Reads an unsigned 32-bit integer from the buffer at
148
+ * the specified offset as little-endian.
149
+ *
150
+ * @param offset - Number of bytes to skip before starting to read.
151
+ * Must satisfy `0 <= offset <= buf.length - 4`.
152
+ * Default: 0.
153
+ * @returns the 32-bit unsigned integer at position `offset`.s
154
+ */
68
155
  readUInt32LE(offset = 0) {
69
156
  if (offset + 4 > this.array.byteLength + this.array.byteOffset) {
70
157
  throw new Error('read past array end');
@@ -75,6 +162,13 @@ class DataViewlet {
75
162
  this.array[offset]) >>>
76
163
  0);
77
164
  }
165
+ /*
166
+ * Read a string at a specified offset.
167
+ *
168
+ * @param offset - to beginning of buffer in bytes
169
+ * @param length - of the string to read in bytes or to the end of the
170
+ * buffer if not specified.
171
+ */
78
172
  readString(offset = 0, length) {
79
173
  if (offset + length >
80
174
  this.array.byteLength + this.array.byteOffset) {
@@ -87,6 +181,15 @@ class DataViewlet {
87
181
  }
88
182
  }
89
183
 
184
+ /*
185
+ * Parse an MO file.
186
+ *
187
+ * An exception is thrown for invalid data.
188
+ *
189
+ * @param raw - The input as either a binary `String`, any `Array`-like byte
190
+ * storage (`Array`, `Uint8Array`, `Arguments`, `jQuery(Array)`, ...)
191
+ * @returns a Catalog
192
+ */
90
193
  function parseMoCatalog(raw) {
91
194
  const catalog = {
92
195
  major: 0,
@@ -99,15 +202,19 @@ function parseMoCatalog(raw) {
99
202
  const magic = blob.readUInt32LE(offset);
100
203
  let reader;
101
204
  if (magic === 0x950412de) {
205
+ /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */
102
206
  reader = (buf, off) => buf.readUInt32LE(off);
103
207
  }
104
208
  else if (magic === 0xde120495) {
209
+ /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */
105
210
  reader = (buf, off) => buf.readUInt32BE(off);
106
211
  }
107
212
  else {
108
213
  throw new Error(`invalid MO magic 0x${magic.toString(16)}`);
109
214
  }
110
215
  offset += 4;
216
+ // The revision is encoded in two shorts, major and minor. We don't care
217
+ // about the minor revision.
111
218
  const major = reader(blob, offset) >> 16;
112
219
  offset += 4;
113
220
  if (major > 0) {
@@ -168,6 +275,10 @@ function parseMoCatalog(raw) {
168
275
  }
169
276
 
170
277
  function validateMoJsonCatalog(udata) {
278
+ // We could use ajv but it results in almost 300 k minimized code
279
+ // for the browser bundle. This validator instead is absolutely
280
+ // minimalistic, and only avoids exceptions that can occur, when
281
+ // accessing entries.
171
282
  if (udata === null || typeof udata === 'undefined') {
172
283
  throw new Error('catalog is either null or undefined');
173
284
  }
@@ -175,6 +286,8 @@ function validateMoJsonCatalog(udata) {
175
286
  if (data.constructor !== Object) {
176
287
  throw new Error('catalog must be a dictionary');
177
288
  }
289
+ // We don't care about major and minor because they are actually not
290
+ // used.
178
291
  if (!Object.prototype.hasOwnProperty.call(data, 'entries')) {
179
292
  throw new Error('catalog.entries does not exist');
180
293
  }
@@ -199,11 +312,16 @@ function parseMoJsonCatalog(json) {
199
312
  }
200
313
 
201
314
  function validateJsonCatalog(udata) {
315
+ // We could use ajv but it results in almost 300 k minimized code
316
+ // for the browser bundle. This validator instead is absolutely
317
+ // minimalistic, and only avoids exceptions that can occur, when
318
+ // accessing entries.
202
319
  if (udata === null || typeof udata === 'undefined') {
203
320
  throw new Error('catalog is either null or undefined');
204
321
  }
205
322
  const entries = udata;
206
323
  if (entries.constructor !== Object) ;
324
+ // Convert to a regular catalog.
207
325
  const catalog = {
208
326
  major: 0,
209
327
  minor: 1,
@@ -211,6 +329,7 @@ function validateJsonCatalog(udata) {
211
329
  entries: {},
212
330
  };
213
331
  for (const [msgid, msgstr] of Object.entries(entries)) {
332
+ // Just stringify all values but do not complain.
214
333
  catalog.entries[msgid] = [msgstr.toString()];
215
334
  }
216
335
  return catalog;
@@ -256,8 +375,19 @@ function splitLocale(locale) {
256
375
  return split;
257
376
  }
258
377
 
378
+ /*
379
+ * Caches catalog lookups by path, locale, and textdomain.
380
+ *
381
+ * Failed lookups are stored as null values.
382
+ *
383
+ * It is also possible to store a Promise. In that case, if a request is
384
+ * made to bind the textdomain, the promise is settled. Note that this
385
+ * mechanism is never used for message lookup but only for loading the
386
+ * catalog via resolve.
387
+ */
259
388
  class CatalogCache {
260
389
  constructor() {
390
+ /* Singleton. */
261
391
  }
262
392
  static getInstance() {
263
393
  if (!CatalogCache.instance) {
@@ -268,6 +398,17 @@ class CatalogCache {
268
398
  static clear() {
269
399
  CatalogCache.cache = {};
270
400
  }
401
+ /**
402
+ * Lookup a Catalog for a given base path, locale, and textdomain.
403
+ *
404
+ * The locale key is usually the locale identifier (e.g. de-DE or sr\@latin).
405
+ * But it can also be a colon separated list of such locale identifiers.
406
+ *
407
+ *
408
+ * @param localeKey - the locale key
409
+ * @param textdomain - the textdomain
410
+ * @returns the cached Catalog, a Promise or null for failure
411
+ */
271
412
  static lookup(localeKey, textdomain) {
272
413
  if (CatalogCache.cache[localeKey]) {
273
414
  const ptr = CatalogCache.cache[localeKey];
@@ -279,6 +420,7 @@ class CatalogCache {
279
420
  }
280
421
  static store(localeKey, textdomain, entry) {
281
422
  if (Promise.resolve(entry) !== entry) {
423
+ // Object.
282
424
  entry = validateMoJsonCatalog(entry);
283
425
  }
284
426
  if (!CatalogCache.cache[localeKey]) {
@@ -320,144 +462,141 @@ function explodeLocale(locale, vary) {
320
462
  }
321
463
 
322
464
  function loadCatalog(url, format) {
323
- let transportInstance;
324
- {
325
- let transport;
326
- try {
327
- const parsedURL = new URL(url);
328
- if (parsedURL.protocol === 'https:' ||
329
- parsedURL.protocol === 'http:' ||
330
- parsedURL.protocol === 'file:') {
331
- transport = 'http';
465
+ return __awaiter(this, void 0, void 0, function* () {
466
+ let transportInstance;
467
+ {
468
+ let transport;
469
+ // Check whether this is a valid URL.
470
+ try {
471
+ const parsedURL = new URL(url);
472
+ if (parsedURL.protocol === 'https:' ||
473
+ parsedURL.protocol === 'http:' ||
474
+ parsedURL.protocol === 'file:') {
475
+ transport = 'http';
476
+ }
477
+ else {
478
+ throw new Error(`unsupported scheme ${parsedURL.protocol}`);
479
+ }
480
+ }
481
+ catch (e) {
482
+ {
483
+ transport = 'fs';
484
+ }
485
+ }
486
+ if (transport === 'http') {
487
+ transportInstance = new TransportHttp();
332
488
  }
333
489
  else {
334
- throw new Error(`unsupported scheme ${parsedURL.protocol}`);
490
+ transportInstance = new TransportFs();
335
491
  }
336
492
  }
337
- catch (e) {
338
- {
339
- transport = 'fs';
340
- }
493
+ let validator;
494
+ if ('mo.json' === format) {
495
+ validator = parseMoJsonCatalog;
341
496
  }
342
- if (transport === 'http') {
343
- transportInstance = new TransportHttp();
497
+ else if ('.json' === format) {
498
+ validator = parseJsonCatalog;
344
499
  }
345
500
  else {
346
- transportInstance = new TransportFs();
501
+ validator = parseMoCatalog;
502
+ }
503
+ try {
504
+ const data = yield transportInstance.loadFile(url);
505
+ return validator(data);
506
+ }
507
+ catch (_a) {
508
+ return null;
347
509
  }
348
- }
349
- let validator;
350
- if ('mo.json' === format) {
351
- validator = parseMoJsonCatalog;
352
- }
353
- else if ('.json' === format) {
354
- validator = parseJsonCatalog;
355
- }
356
- else {
357
- validator = parseMoCatalog;
358
- }
359
- return new Promise((resolve, reject) => {
360
- transportInstance
361
- .loadFile(url)
362
- .then(data => {
363
- resolve(validator(data));
364
- })
365
- .catch(e => reject(e));
366
510
  });
367
511
  }
368
512
  function assemblePath(base, id, domainname, extender) {
369
513
  return `${base}/${id}/LC_MESSAGES/${domainname}.${extender}`;
370
514
  }
371
515
  function loadLanguageFromObject(ids, base, domainname) {
372
- let catalog;
373
- for (let i = 0; i < ids.length; ++i) {
374
- const id = ids[0];
375
- if (!Object.prototype.hasOwnProperty.call(base, id)) {
376
- continue;
377
- }
378
- if (!Object.prototype.hasOwnProperty.call(base[id], 'LC_MESSAGES')) {
379
- continue;
380
- }
381
- if (!Object.prototype.hasOwnProperty.call(base[id].LC_MESSAGES, domainname)) {
382
- continue;
383
- }
384
- catalog = base[id].LC_MESSAGES[domainname];
385
- }
386
- return new Promise((resolve, reject) => {
387
- if (catalog) {
388
- resolve(catalog);
389
- }
390
- else {
391
- reject();
516
+ return __awaiter(this, void 0, void 0, function* () {
517
+ for (let i = 0; i < ids.length; ++i) {
518
+ const id = ids[i];
519
+ // Language exists?
520
+ if (!Object.prototype.hasOwnProperty.call(base, id)) {
521
+ continue;
522
+ }
523
+ // LC_MESSAGES?
524
+ if (!Object.prototype.hasOwnProperty.call(base[id], 'LC_MESSAGES')) {
525
+ continue;
526
+ }
527
+ // Textdomain?
528
+ if (!Object.prototype.hasOwnProperty.call(base[id].LC_MESSAGES, domainname)) {
529
+ continue;
530
+ }
531
+ return base[id].LC_MESSAGES[domainname];
392
532
  }
533
+ return null;
393
534
  });
394
535
  }
395
- async function loadLanguage(ids, base, domainname, format) {
396
- if (typeof base === 'object' && base !== null) {
397
- return loadLanguageFromObject(ids, base, domainname);
398
- }
399
- return new Promise((resolve, reject) => {
400
- const tries = new Array();
401
- ids.forEach(id => {
402
- tries.push(() => loadCatalog(assemblePath(base, id, domainname, format), format));
403
- });
404
- tries
405
- .reduce((promise, fn) => promise.catch(fn), Promise.reject())
406
- .then(value => resolve(value))
407
- .catch(e => reject(e));
536
+ /*
537
+ * First tries to load a catalog with the specified charset, then with the
538
+ * charset converted to uppercase (if it differs from the original charset),
539
+ * and finally without a charset.
540
+ */
541
+ function loadLanguage(ids, base, domainname, format) {
542
+ return __awaiter(this, void 0, void 0, function* () {
543
+ // Check if `base` is an object (LocaleContainer).
544
+ if (typeof base === 'object' && base !== null) {
545
+ return loadLanguageFromObject(ids, base, domainname);
546
+ }
547
+ for (const id of ids) {
548
+ const catalog = yield loadCatalog(assemblePath(base, id, domainname, format), format);
549
+ if (catalog) {
550
+ return catalog;
551
+ }
552
+ }
553
+ return null;
408
554
  });
409
555
  }
410
- async function loadDomain(exploded, localeKey, base, domainname, format) {
411
- const entries = {};
412
- const catalog = {
413
- major: 0,
414
- minor: 0,
415
- pluralFunction: germanicPlural,
416
- entries,
417
- };
418
- const cacheHit = CatalogCache.lookup(localeKey, domainname);
419
- if (cacheHit !== null) {
420
- if (Promise.resolve(cacheHit) === cacheHit) {
556
+ function loadDomain(exploded, localeKey, base, domainname, format) {
557
+ return __awaiter(this, void 0, void 0, function* () {
558
+ const entries = {};
559
+ const catalog = {
560
+ major: 0,
561
+ minor: 0,
562
+ pluralFunction: germanicPlural,
563
+ entries,
564
+ };
565
+ const cacheHit = yield CatalogCache.lookup(localeKey, domainname);
566
+ if (cacheHit !== null) {
421
567
  return cacheHit;
422
568
  }
423
- else {
424
- return new Promise(resolve => resolve(cacheHit));
569
+ for (const tries of exploded) {
570
+ const result = yield loadLanguage(tries, base, domainname, format);
571
+ if (result) {
572
+ catalog.major = result.major;
573
+ catalog.minor = result.minor;
574
+ catalog.entries = Object.assign(Object.assign({}, catalog.entries), result.entries);
575
+ }
425
576
  }
426
- }
427
- const promises = new Array();
428
- const results = new Array();
429
- exploded.forEach((tries, i) => {
430
- const p = loadLanguage(tries, base, domainname, format)
431
- .then(catalog => (results[i] = catalog))
432
- .catch(() => {
433
- });
434
- promises.push(p);
435
- });
436
- await Promise.all(promises);
437
- results.forEach(result => {
438
- catalog.major = result.major;
439
- catalog.minor = result.minor;
440
- catalog.entries = Object.assign(Object.assign({}, catalog.entries), result.entries);
441
- });
442
- return new Promise(resolve => {
443
- resolve(catalog);
577
+ return catalog;
444
578
  });
445
579
  }
446
580
  function pluralExpression(str) {
447
581
  const tokens = str
448
582
  .replace(/[ \t\r\013\014]/g, '')
449
583
  .replace(/;$/, '')
584
+ // Do NOT allow square brackets here. JSFuck!
450
585
  .split(/[<>!=]=|&&|\|\||[-!*/%+<>=?:;]/);
451
586
  for (let i = 0; i < tokens.length; ++i) {
452
587
  const token = tokens[i].replace(/^\(+/, '').replace(/\)+$/, '');
453
588
  if (token !== 'nplurals' &&
454
589
  token !== 'plural' &&
455
590
  token !== 'n' &&
591
+ // Does not catch invalid octal numbers but the compiler
592
+ // takes care of that.
456
593
  null === /^[0-9]+$/.exec(token)) {
457
594
  throw new Error('invalid plural function');
458
595
  }
459
596
  }
460
597
  const code = 'var nplurals = 1, plural = 0;' + str + '; return 0 + plural';
598
+ // This may throw an exception!
599
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
461
600
  return new Function('n', code);
462
601
  }
463
602
  function setPluralFunction(catalog) {
@@ -469,33 +608,32 @@ function setPluralFunction(catalog) {
469
608
  const tokens = header.split(':');
470
609
  if ('plural-forms' === tokens.shift().toLowerCase()) {
471
610
  const code = tokens.join(':');
472
- catalog.pluralFunction = pluralExpression(code);
611
+ try {
612
+ catalog.pluralFunction = pluralExpression(code);
613
+ }
614
+ catch (_a) {
615
+ catalog.pluralFunction = germanicPlural;
616
+ }
473
617
  }
474
618
  });
475
619
  return catalog;
476
620
  }
477
621
  function resolveImpl(domainname, path, format, localeKey) {
478
- const defaultCatalog = {
479
- major: 0,
480
- minor: 0,
481
- pluralFunction: germanicPlural,
482
- entries: {},
483
- };
484
- if (localeKey === 'C' || localeKey === 'POSIX') {
485
- return new Promise(resolve => resolve(defaultCatalog));
486
- }
487
- return new Promise(resolve => {
622
+ return __awaiter(this, void 0, void 0, function* () {
623
+ const defaultCatalog = {
624
+ major: 0,
625
+ minor: 0,
626
+ pluralFunction: germanicPlural,
627
+ entries: {},
628
+ };
629
+ if (localeKey === 'C' || localeKey === 'POSIX') {
630
+ return defaultCatalog;
631
+ }
488
632
  const exploded = explodeLocale(splitLocale(localeKey));
489
- loadDomain(exploded, localeKey, path, domainname, format)
490
- .then(catalog => {
491
- setPluralFunction(catalog);
492
- CatalogCache.store(localeKey, domainname, catalog);
493
- resolve(catalog);
494
- })
495
- .catch(() => {
496
- CatalogCache.store(localeKey, domainname, defaultCatalog);
497
- resolve(defaultCatalog);
498
- });
633
+ const catalog = yield loadDomain(exploded, localeKey, path, domainname, format);
634
+ setPluralFunction(catalog);
635
+ CatalogCache.store(localeKey, domainname, catalog);
636
+ return catalog;
499
637
  });
500
638
  }
501
639
 
@@ -575,10 +713,50 @@ function selectLocale(supported, requested) {
575
713
  return 'C';
576
714
  }
577
715
 
716
+ /**
717
+ * A Textdomain is a container for an esgettext configuration and all loaded
718
+ * LocaleContainer for the textual domain selected.
719
+ *
720
+ * The actual translation methods have quite funny names like `_()` or
721
+ * `_x()`. The purpose of this naming convention is to make the
722
+ * internationalization of your programs as little obtrusive as possible.
723
+ * Most of the times you just have to exchange
724
+ *
725
+ * ```
726
+ * doSomething('Hello, world!');
727
+ * ```
728
+ *
729
+ * with
730
+ *
731
+ * ```
732
+ * doSomething(gtx._('Hello, world!'));
733
+ * ```
734
+ *
735
+ * Besides, depending on the string extractor you are using, it may be useful
736
+ * that the method names do not collide with method names from other packages.
737
+ */
578
738
  class Textdomain {
739
+ /**
740
+ * Retrieve a translation for a string.
741
+ *
742
+ * @param msgid - the string to translate
743
+ *
744
+ * @returns the translated string
745
+ */
579
746
  _(msgid) {
580
747
  return gettextImpl({ msgid: msgid, catalog: this.catalog });
581
748
  }
749
+ /**
750
+ * Retrieve a translation for a string containing a possible plural.
751
+ * You will almost always want to call {@link _nx} instead so that
752
+ * you can interpolate the number of items into the strings.
753
+ *
754
+ * @param msgid - the string in the singular
755
+ * @param msgidPlural - the string in the plural
756
+ * @param numItems - the number of items
757
+ *
758
+ * @returns the translated string
759
+ */
582
760
  _n(msgid, msgidPlural, numItems) {
583
761
  return gettextImpl({
584
762
  msgid: msgid,
@@ -587,6 +765,14 @@ class Textdomain {
587
765
  catalog: this.catalog,
588
766
  });
589
767
  }
768
+ /**
769
+ * Translate a string with a context.
770
+ *
771
+ * @param msgctxt - the message context
772
+ * @param msgid - the string to translate
773
+ *
774
+ * @returns the translated string
775
+ */
590
776
  _p(msgctxt, msgid) {
591
777
  return gettextImpl({
592
778
  msgctxt: msgctxt,
@@ -594,6 +780,17 @@ class Textdomain {
594
780
  catalog: this.catalog,
595
781
  });
596
782
  }
783
+ /**
784
+ * The method `_np()` combines `_n()` with `_p()`.
785
+ * You will almost always want to call {@link _npx} instead so that
786
+ * you can interpolate the number of items into the strings.
787
+
788
+ *
789
+ * @param msgctxt - the message context
790
+ * @param msgid - the message id
791
+ * @param placeholders - a dictionary with placehoders
792
+ * @returns the translated string
793
+ */
597
794
  _np(msgctxt, msgid, msgidPlural, numItems) {
598
795
  return gettextImpl({
599
796
  msgctxt: msgctxt,
@@ -603,9 +800,29 @@ class Textdomain {
603
800
  catalog: this.catalog,
604
801
  });
605
802
  }
803
+ /**
804
+ * Translate a string with placeholders. The placeholders should be
805
+ * wrapped into curly braces and must match the regular expression
806
+ * "[_a-zA-Z][_a-zA-Z0-9]*".
807
+ *
808
+ * @param msgid - the msgid to translate
809
+ * @param placeholders - an optional dictionary of placeholders
810
+ *
811
+ * @returns the translated string with placeholders expanded
812
+ */
606
813
  _x(msgid, placeholders) {
607
814
  return Textdomain.expand(gettextImpl({ msgid: msgid, catalog: this.catalog }), placeholders || {});
608
815
  }
816
+ /**
817
+ * Translate a string with a plural expression with placeholders.
818
+ *
819
+ * @param msgid - the string in the singular
820
+ * @param msgidPlural - the string in the plural
821
+ * @param numItems - the number of items
822
+ * @param placeholders - an optional dictionary of placeholders
823
+ *
824
+ * @returns the translated string
825
+ */
609
826
  _nx(msgid, msgidPlural, numItems, placeholders) {
610
827
  return Textdomain.expand(gettextImpl({
611
828
  msgid: msgid,
@@ -614,9 +831,28 @@ class Textdomain {
614
831
  catalog: this.catalog,
615
832
  }), placeholders || {});
616
833
  }
834
+ /**
835
+ * The method `_px()` combines `_p()` with `_x()`.
836
+ *
837
+ * @param msgctxt - the message context
838
+ * @param msgid - the message id
839
+ * @param placeholders - an optional dictionary with placehoders
840
+ * @returns the translated string
841
+ */
617
842
  _px(msgctxt, msgid, placeholders) {
618
843
  return Textdomain.expand(gettextImpl({ msgctxt: msgctxt, msgid: msgid, catalog: this.catalog }), placeholders || {});
619
844
  }
845
+ /**
846
+ * The method `_npx()` brings it all together. It combines `_n()` and
847
+ * `_p()` and `_x()`.
848
+ *
849
+ * @param msgctxt - the message context
850
+ * @param msgid - the message id
851
+ * @param msgidPlural - the plural string
852
+ * @param numItems - the number of items
853
+ * @param placeholders - an optional dictionary with placehoders
854
+ * @returns the translated string
855
+ */
620
856
  _npx(msgctxt, msgid, msgidPlural, numItems, placeholders) {
621
857
  return Textdomain.expand(gettextImpl({
622
858
  msgctxt: msgctxt,
@@ -638,10 +874,31 @@ class Textdomain {
638
874
  }
639
875
  return catalog;
640
876
  }
877
+ /**
878
+ * Retrieve a translation for a string with a fixed locale.
879
+ *
880
+ * @param locale - the locale identifier
881
+ * @param msgid - the string to translate
882
+ *
883
+ * @returns the translated string
884
+ */
641
885
  _l(locale, msgid) {
642
886
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
643
887
  return gettextImpl({ msgid: msgid, catalog: catalog });
644
888
  }
889
+ /**
890
+ * Retrieve a translation for a string containing a possible plural with
891
+ * a fixed locale.
892
+ * You will almost always want to call {@link _nx} instead so that
893
+ * you can interpolate the number of items into the strings.
894
+ *
895
+ * @param locale - the locale identifier
896
+ * @param msgid - the string in the singular
897
+ * @param msgidPlural - the string in the plural
898
+ * @param numItems - the number of items
899
+ *
900
+ * @returns the translated string
901
+ */
645
902
  _ln(locale, msgid, msgidPlural, numItems) {
646
903
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
647
904
  return gettextImpl({
@@ -651,10 +908,31 @@ class Textdomain {
651
908
  catalog: catalog,
652
909
  });
653
910
  }
911
+ /**
912
+ * Translate a string with a context with a fixed locale.
913
+ *
914
+ * @param locale - the locale identifier
915
+ * @param msgctxt - the message context
916
+ * @param msgid - the string to translate
917
+ *
918
+ * @returns the translated string
919
+ */
654
920
  _lp(locale, msgctxt, msgid) {
655
921
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
656
922
  return gettextImpl({ msgctxt: msgctxt, msgid: msgid, catalog: catalog });
657
923
  }
924
+ /**
925
+ * The method `_lnp()` combines `_ln()` with `_lp()`.
926
+ * You will almost always want to call {@link _npx} instead so that
927
+ * you can interpolate the number of items into the strings.
928
+
929
+ *
930
+ * @param locale - the locale identifier
931
+ * @param msgctxt - the message context
932
+ * @param msgid - the message id
933
+ * @param placeholders - a dictionary with placehoders
934
+ * @returns the translated string
935
+ */
658
936
  _lnp(locale, msgctxt, msgid, msgidPlural, numItems) {
659
937
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
660
938
  return gettextImpl({
@@ -665,10 +943,34 @@ class Textdomain {
665
943
  catalog: catalog,
666
944
  });
667
945
  }
946
+ /**
947
+ * Translate a string with placeholders for a fixed locale.
948
+ * The placeholders should be
949
+ * wrapped into curly braces and must match the regular expression
950
+ * "[_a-zA-Z][_a-zA-Z0-9]*".
951
+ *
952
+ * @param locale - the locale identifier
953
+ * @param msgid - the msgid to translate
954
+ * @param placeholders - an optional dictionary of placeholders
955
+ *
956
+ * @returns the translated string with placeholders expanded
957
+ */
668
958
  _lx(locale, msgid, placeholders) {
669
959
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
670
960
  return Textdomain.expand(gettextImpl({ msgid: msgid, catalog: catalog }), placeholders || {});
671
961
  }
962
+ /**
963
+ * Translate a string with a plural expression with placeholders into a
964
+ * fixed locale.
965
+ *
966
+ * @param locale - the locale identifier
967
+ * @param msgid - the string in the singular
968
+ * @param msgidPlural - the string in the plural
969
+ * @param numItems - the number of items
970
+ * @param placeholders - an optional dictionary of placeholders
971
+ *
972
+ * @returns the translated string
973
+ */
672
974
  _lnx(locale, msgid, msgidPlural, numItems, placeholders) {
673
975
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
674
976
  return Textdomain.expand(gettextImpl({
@@ -678,10 +980,31 @@ class Textdomain {
678
980
  catalog: catalog,
679
981
  }), placeholders || {});
680
982
  }
983
+ /**
984
+ * The method `_lpx()` combines `_lp()` with `_lx()`.
985
+ *
986
+ * @param locale - the locale identifier
987
+ * @param msgctxt - the message context
988
+ * @param msgid - the message id
989
+ * @param placeholders - an optional dictionary with placehoders
990
+ * @returns the translated string
991
+ */
681
992
  _lpx(locale, msgctxt, msgid, placeholders) {
682
993
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
683
994
  return Textdomain.expand(gettextImpl({ msgctxt: msgctxt, msgid: msgid, catalog: catalog }), placeholders || {});
684
995
  }
996
+ /**
997
+ * The method `_lnpx()` brings it all together. It combines `_ln()` and
998
+ * `_lp()` and `_lx()`.
999
+ *
1000
+ * @param locale - the locale identifier
1001
+ * @param msgctxt - the message context
1002
+ * @param msgid - the message id
1003
+ * @param msgidPlural - the plural string
1004
+ * @param numItems - the number of items
1005
+ * @param placeholders - an optional dictionary with placehoders
1006
+ * @returns the translated string
1007
+ */
685
1008
  _lnpx(locale, msgctxt, msgid, msgidPlural, numItems, placeholders) {
686
1009
  const catalog = Textdomain.getCatalog(locale, this.textdomain());
687
1010
  return Textdomain.expand(gettextImpl({
@@ -702,6 +1025,14 @@ class Textdomain {
702
1025
  }
703
1026
  });
704
1027
  }
1028
+ /**
1029
+ * Instantiate a Textdomain object. Textdomain objects are singletons
1030
+ * for each textdomain identifier.
1031
+ *
1032
+ * @param textdomain - the textdomain of your application or library.
1033
+ *
1034
+ * @returns a [[`Textdomain`]]
1035
+ */
705
1036
  static getInstance(textdomain) {
706
1037
  if (typeof textdomain === 'undefined' ||
707
1038
  textdomain === null ||
@@ -723,16 +1054,49 @@ class Textdomain {
723
1054
  return domain;
724
1055
  }
725
1056
  }
1057
+ /**
1058
+ * Delete all existing singletons. This method should usually be called
1059
+ * only, when you want to free memory.
1060
+ */
726
1061
  static clearInstances() {
727
1062
  Textdomain.boundDomains = {};
728
1063
  }
1064
+ /**
1065
+ * This method is used for testing. Do not use it yourself!
1066
+ */
729
1067
  static forgetInstances() {
730
1068
  Textdomain.clearInstances();
731
1069
  Textdomain.domains = {};
732
1070
  }
1071
+ /**
1072
+ * Query the locale in use.
1073
+ */
733
1074
  static get locale() {
734
1075
  return Textdomain._locale;
735
1076
  }
1077
+ /**
1078
+ * Change the locale.
1079
+ *
1080
+ * For the web you can use all valid language identifier tags that
1081
+ * [BCP47](https://tools.ietf.org/html/bcp47) allows
1082
+ * (and actually a lot more). The tag is always used unmodified.
1083
+ *
1084
+ * For server environments, the locale identifier has to match the following
1085
+ * scheme:
1086
+ *
1087
+ * `ll_CC.charset\@modifier`
1088
+ *
1089
+ * * `ll` is the two- or three-letter language code.
1090
+ * * `CC` is the optional two-letter country code.
1091
+ * * `charset` is an optional character set (letters, digits, and the hyphen).
1092
+ * * `modifier` is an optional variant (letters and digits).
1093
+ *
1094
+ * The language code is always converted to lowercase, the country code is
1095
+ * converted to uppercase, variant and charset are used as is.
1096
+ *
1097
+ * @param locale - the locale identifier
1098
+ * @returns the locale in use
1099
+ */
736
1100
  static set locale(locale) {
737
1101
  const ucLocale = locale.toUpperCase();
738
1102
  if (ucLocale === 'POSIX' || ucLocale === 'C') {
@@ -743,6 +1107,7 @@ class Textdomain {
743
1107
  if (!split) {
744
1108
  throw new Error('invalid locale identifier');
745
1109
  }
1110
+ // Node.
746
1111
  split.tags[0] = split.tags[0].toLowerCase();
747
1112
  if (split.tags.length > 1) {
748
1113
  split.tags[1] = split.tags[1].toUpperCase();
@@ -758,46 +1123,110 @@ class Textdomain {
758
1123
  }
759
1124
  constructor(domain) {
760
1125
  this._catalogFormat = 'mo.json';
1126
+ this.catalog = undefined;
761
1127
  this.domain = domain;
1128
+ const msg = "The property 'locale' is not an instance property but static. Use 'Textdomain.locale' instead!";
1129
+ Object.defineProperty(this, 'locale', {
1130
+ get: () => {
1131
+ throw new Error(msg);
1132
+ },
1133
+ set: () => {
1134
+ throw new Error(msg);
1135
+ },
1136
+ enumerable: true,
1137
+ configurable: true,
1138
+ });
762
1139
  }
1140
+ /**
1141
+ * A textdomain is an identifier for your application or library. It is
1142
+ * the basename of your translation files which are either
1143
+ * TEXTDOMAIN.mo.json or TEXTDOMAIN.mo, depending on the format you have
1144
+ * chosen.
1145
+ *
1146
+ * FIXME! This should be a getter!
1147
+ *
1148
+ * @returns the textdomain
1149
+ */
763
1150
  textdomain() {
764
1151
  return this.domain;
765
1152
  }
1153
+ /**
1154
+ * Bind a textdomain to a certain path or queries the path that a
1155
+ * textdomain is bound to. The catalog file will be searched
1156
+ * in `${path}/LC_MESSAGES/${domainname}.EXT` where `EXT` is the
1157
+ * selected catalog format (one of `mo.json`, `mo`, or `json`).
1158
+ *
1159
+ * Alternatively, you can pass a [[`LocaleContainer`]] that holds the
1160
+ * catalogs in memory.
1161
+ *
1162
+ * The returned string or `LocaleContainer` is valid until the next
1163
+ * `bindtextdomain` call with an argument.
1164
+ *
1165
+ * @param path - the base path or [[`LocaleContainer`]] for this textdomain
1166
+ *
1167
+ * @returns the current base directory or [[`LocaleContainer`]] for this domain, after possibly changing it.
1168
+ */
766
1169
  bindtextdomain(path) {
767
1170
  if (typeof path !== 'undefined') {
768
1171
  Textdomain.boundDomains[this.domain] = path;
769
1172
  }
770
1173
  return Textdomain.boundDomains[this.domain];
771
1174
  }
772
- async resolve(locale) {
773
- const promises = [this.resolve1(locale)];
774
- for (const td in Textdomain.domains) {
775
- if (Object.prototype.hasOwnProperty.call(Textdomain.domains, td) &&
776
- Textdomain.domains[td] !== this) {
777
- promises.push(Textdomain.domains[td].resolve1(locale));
1175
+ /**
1176
+ * Resolve a textdomain, i.e. load the LocaleContainer for this domain and all
1177
+ * of its dependencies for the currently selected locale or the locale
1178
+ * specified.
1179
+ *
1180
+ * The promise will always resolve. If no catalog was found, an empty
1181
+ * catalog will be returned that is still usable.
1182
+ *
1183
+ * @param locale - an optional locale identifier, defaults to Textdomain.locale
1184
+ *
1185
+ * @returns a promise for a Catalog that will always resolve.
1186
+ */
1187
+ resolve(locale) {
1188
+ return __awaiter(this, void 0, void 0, function* () {
1189
+ const promises = [this.resolve1(locale)];
1190
+ for (const td in Textdomain.domains) {
1191
+ if (Object.prototype.hasOwnProperty.call(Textdomain.domains, td) &&
1192
+ Textdomain.domains[td] !== this) {
1193
+ promises.push(Textdomain.domains[td].resolve1(locale));
1194
+ }
778
1195
  }
779
- }
780
- return Promise.all(promises).then(values => {
781
- return new Promise(resolve => resolve(values[0]));
1196
+ return Promise.all(promises).then(values => {
1197
+ return new Promise(resolve => resolve(values[0]));
1198
+ });
782
1199
  });
783
1200
  }
784
- async resolve1(locale) {
785
- let path = this.bindtextdomain();
786
- if (typeof path === 'undefined' || path === null) {
787
- const parts = ['.', 'locale'];
788
- path = parts.join(pathSeparator);
789
- }
790
- const resolvedLocale = locale ? locale : Textdomain.locale;
791
- return resolveImpl(this.domain, path, this.catalogFormat, resolvedLocale).then(catalog => {
792
- if (!locale) {
793
- this.catalog = catalog;
1201
+ resolve1(locale) {
1202
+ return __awaiter(this, void 0, void 0, function* () {
1203
+ let path = this.bindtextdomain();
1204
+ if (typeof path === 'undefined' || path === null) {
1205
+ const parts = ['.', 'locale'];
1206
+ path = parts.join(pathSeparator);
794
1207
  }
795
- return new Promise(resolve => resolve(catalog));
1208
+ const resolvedLocale = locale ? locale : Textdomain.locale;
1209
+ return resolveImpl(this.domain, path, this.catalogFormat, resolvedLocale).then(catalog => {
1210
+ if (!locale) {
1211
+ this.catalog = catalog;
1212
+ }
1213
+ return new Promise(resolve => resolve(catalog));
1214
+ });
796
1215
  });
797
1216
  }
1217
+ /**
1218
+ * Get the catalog format in use.
1219
+ *
1220
+ * @returns one of 'mo.json' or 'mo' (default is 'mo.json')
1221
+ */
798
1222
  get catalogFormat() {
799
1223
  return this._catalogFormat;
800
1224
  }
1225
+ /**
1226
+ * Set the catalog format to use.
1227
+ *
1228
+ * @param format - one of 'mo.json' or 'mo'
1229
+ */
801
1230
  set catalogFormat(format) {
802
1231
  format = format.toLowerCase();
803
1232
  if (format === 'mo.json') {
@@ -810,42 +1239,156 @@ class Textdomain {
810
1239
  throw new Error(`unsupported format ${format}`);
811
1240
  }
812
1241
  }
1242
+ /**
1243
+ * Queries the user's preferred locales. On the server it queries the
1244
+ * environment variables `LANGUAGE`, `LC_ALL`, `LANG`, and `LC_MESSAGES`
1245
+ * (in that order). In the browser, it parses it checks the user preferences
1246
+ * in the variables `navigator.languages`, `navigator.language`,
1247
+ * `navigator.userLanguage`, `navigator.browserLanguage`, and
1248
+ * `navigator.systemLanguage`.
1249
+ *
1250
+ * @returns the set of locales in order of preference
1251
+ *
1252
+ * Added in \@runtime 0.1.0.
1253
+ */
813
1254
  static userLocales() {
814
1255
  return userLocales();
815
1256
  }
1257
+ /**
1258
+ * Select one of the supported locales from a list of locales accepted by
1259
+ * the user.
1260
+ *
1261
+ * @param supported - the list of locales supported by the application
1262
+ * @param requested - the list of locales accepted by the user
1263
+ *
1264
+ * If called with just one argument, then the list of requested locales
1265
+ * is determined by calling [[Textdomain.userLocales]].
1266
+ *
1267
+ * @returns the negotiated locale or 'C' if not possible.
1268
+ */
816
1269
  static selectLocale(supported, requested) {
817
1270
  return selectLocale(supported, requested !== null && requested !== void 0 ? requested : Textdomain.userLocales());
818
1271
  }
1272
+ /**
1273
+ * A no-op method for string marking.
1274
+ *
1275
+ * Sometimes you want to mark strings for translation but do not actually
1276
+ * want to translate them, at least not at the time of their definition.
1277
+ * This is often the case, when you have to preserve the original string.
1278
+ *
1279
+ * Take this example:
1280
+ *
1281
+ * ```
1282
+ * orangeColors = [gtx.N_('coral'), gtx.N_('tomato'), gtx.N_('orangered'),
1283
+ * gtx.N_('gold'), gtx.N_('orange'), gtx.N_('darkorange')]
1284
+ * ```
1285
+ *
1286
+ * These are standard CSS colors, and you cannot translate them inside
1287
+ * CSS styles. But for presentation you may want to translate them later:
1288
+ *
1289
+ * ```
1290
+ * console.log(gtx._x("The css color '{color}' is {translated}.",
1291
+ * {
1292
+ * color: orangeColors[2],
1293
+ * translated: gtx._(orangeColors[2]),
1294
+ * }
1295
+ * )
1296
+ * );
1297
+ * ```
1298
+ *
1299
+ * In other words: The method just marks strings for translation, so that
1300
+ * the extractor `esgettext-xgettext` finds them but it does not actually
1301
+ * translate anything.
1302
+ *
1303
+ * Similar methods are available for other cases (with placeholder
1304
+ * expansion, context, or both). They are *not* available for plural
1305
+ * methods because that would not make sense.
1306
+ *
1307
+ * Note that all of these methods are also available as instance methods.
1308
+ *
1309
+ * @param msgid - the message id
1310
+ * @returns the original string
1311
+ */
819
1312
  static N_(msgid) {
820
1313
  return msgid;
821
1314
  }
1315
+ /**
1316
+ * Does the same as the static method `N_()`.
1317
+ *
1318
+ * @param msgid - the message id
1319
+ * @returns the original string
1320
+ */
822
1321
  N_(msgid) {
823
1322
  return msgid;
824
1323
  }
1324
+ /**
1325
+ * Same as `N_()` but with placeholder expansion.
1326
+ *
1327
+ * @param msgid - the message id
1328
+ * @param placeholders - a dictionary of placeholders
1329
+ * @returns the original string with placeholders expanded
1330
+ */
825
1331
  N_x(msgid, placeholders) {
826
1332
  return Textdomain.expand(msgid, placeholders !== null && placeholders !== void 0 ? placeholders : {});
827
1333
  }
1334
+ /**
1335
+ * Does the same as the static method `N_x()`.
1336
+ *
1337
+ * @param msgid - the message id
1338
+ * @param placeholders - a dictionary of placeholders
1339
+ * @returns the original string with placeholders expanded
1340
+ */
828
1341
  static N_x(msgid, placeholders) {
829
1342
  return Textdomain.expand(msgid, placeholders !== null && placeholders !== void 0 ? placeholders : {});
830
1343
  }
1344
+ /**
1345
+ * Same as `N_()` but with context.
1346
+ *
1347
+ * @param _msgctxt - the message context (not used)
1348
+ * @param msgid - the message id
1349
+ * @returns the original string
1350
+ */
831
1351
  N_p(_msgctxt, msgid) {
832
1352
  return msgid;
833
1353
  }
1354
+ /**
1355
+ * Does the same as the static method `N_p()`.
1356
+ *
1357
+ * @param _msgctxt - the message context (not used)
1358
+ * @param msgid - the message id
1359
+ * @returns the original string with placeholders expanded
1360
+ */
834
1361
  static N_p(_msgctxt, msgid) {
835
1362
  return msgid;
836
1363
  }
1364
+ /**
1365
+ * Same as `N_()` but with context and placeholder expansion.
1366
+ *
1367
+ * @param _msgctxt - the message context (not used)
1368
+ * @param msgid - the message id
1369
+ * @param placeholders - a dictionary of placeholders
1370
+ * @returns the original string with placeholders expanded
1371
+ */
837
1372
  N_px(_msgctxt, msgid, placeholders) {
838
1373
  return Textdomain.expand(msgid, placeholders !== null && placeholders !== void 0 ? placeholders : {});
839
1374
  }
1375
+ /**
1376
+ * Does the same as the static method `N_px()`.
1377
+ *
1378
+ * @param _msgctxt - the message context (not used)
1379
+ * @param msgid - the message id
1380
+ * @param placeholders - a dictionary of placeholders
1381
+ * @returns the original string with placeholders expanded
1382
+ */
840
1383
  static N_px(_msgctxt, msgid, placeholders) {
841
1384
  return Textdomain.expand(msgid, placeholders !== null && placeholders !== void 0 ? placeholders : {});
842
1385
  }
843
1386
  }
844
1387
  Textdomain.domains = {};
845
- Textdomain.cache = CatalogCache.getInstance();
846
1388
  Textdomain.boundDomains = {};
847
1389
  Textdomain._locale = 'C';
848
1390
 
1391
+ // FIXME! Windows!
849
1392
  if (typeof process.env.LANGUAGE !== 'undefined') {
850
1393
  userLocales(process.env.LANGUAGE.split(':'));
851
1394
  }