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