1 | /* eslint-disable class-methods-use-this, no-underscore-dangle */
|
2 |
|
3 | import get from "lodash/get";
|
4 | import has from "lodash/has";
|
5 | import merge from "lodash/merge";
|
6 |
|
7 | import {
|
8 | DateTime,
|
9 | Dict,
|
10 | FormatNumberOptions,
|
11 | I18nOptions,
|
12 | MissingPlaceholderHandler,
|
13 | NullPlaceholderHandler,
|
14 | NumberToCurrencyOptions,
|
15 | NumberToDelimitedOptions,
|
16 | NumberToHumanOptions,
|
17 | NumberToHumanSizeOptions,
|
18 | NumberToPercentageOptions,
|
19 | NumberToRoundedOptions,
|
20 | Numeric,
|
21 | OnChangeHandler,
|
22 | Scope,
|
23 | StrftimeOptions,
|
24 | TimeAgoInWordsOptions,
|
25 | ToSentenceOptions,
|
26 | TranslateOptions,
|
27 | } from "./typing";
|
28 | import { Locales } from "./Locales";
|
29 | import { Pluralization } from "./Pluralization";
|
30 | import { MissingTranslation } from "./MissingTranslation";
|
31 | import {
|
32 | camelCaseKeys,
|
33 | createTranslationOptions,
|
34 | formatNumber,
|
35 | getFullScope,
|
36 | inferType,
|
37 | interpolate,
|
38 | isSet,
|
39 | lookup,
|
40 | numberToDelimited,
|
41 | numberToHuman,
|
42 | numberToHumanSize,
|
43 | parseDate,
|
44 | pluralize,
|
45 | strftime,
|
46 | timeAgoInWords,
|
47 | } from "./helpers";
|
48 |
|
49 | const DEFAULT_I18N_OPTIONS: I18nOptions = {
|
50 | defaultLocale: "en",
|
51 | locale: "en",
|
52 | defaultSeparator: ".",
|
53 | placeholder: /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm,
|
54 | enableFallback: false,
|
55 | missingBehavior: "message",
|
56 | missingTranslationPrefix: "",
|
57 |
|
58 | missingPlaceholder: (_i18n: I18n, placeholder: string): string =>
|
59 | `[missing "${placeholder}" value]`,
|
60 |
|
61 | nullPlaceholder: (
|
62 | i18n: I18n,
|
63 | placeholder,
|
64 | message: string,
|
65 | options: Dict,
|
66 | ): string => i18n.missingPlaceholder(i18n, placeholder, message, options),
|
67 |
|
68 | transformKey: (key: string): string => key,
|
69 | };
|
70 |
|
71 | export class I18n {
|
72 | private _locale: string = DEFAULT_I18N_OPTIONS.locale;
|
73 | private _defaultLocale: string = DEFAULT_I18N_OPTIONS.defaultLocale;
|
74 | private _version = 0;
|
75 |
|
76 | /**
|
77 | * List of all onChange handlers.
|
78 | *
|
79 | * @type {OnChangeHandler[]}
|
80 | */
|
81 | public onChangeHandlers: OnChangeHandler[] = [];
|
82 |
|
83 | /**
|
84 | * Set the default string separator. Defaults to `.`, as in
|
85 | * `scope.translation`.
|
86 | *
|
87 | * @type {string}
|
88 | */
|
89 | public defaultSeparator: string;
|
90 |
|
91 | /**
|
92 | * Set if engine should fallback to the default locale when a translation is
|
93 | * missing. Defaults to `false`.
|
94 | *
|
95 | * When enabled, missing translations will first be looked for in less
|
96 | * specific versions of the requested locale and if that fails by taking them
|
97 | * from your `I18n#defaultLocale`.
|
98 | *
|
99 | * @type {boolean}
|
100 | */
|
101 | public enableFallback: boolean;
|
102 |
|
103 | /**
|
104 | * The locale resolver registry.
|
105 | *
|
106 | * @see {@link Locales}
|
107 | *
|
108 | * @type {Locales}
|
109 | */
|
110 | public locales: Locales;
|
111 |
|
112 | /**
|
113 | * The pluralization behavior registry.
|
114 | *
|
115 | * @see {@link Pluralization}
|
116 | *
|
117 | * @type {Pluralization}
|
118 | */
|
119 | public pluralization: Pluralization;
|
120 |
|
121 | /**
|
122 | * Set missing translation behavior.
|
123 | *
|
124 | * - `message` will display a message that the translation is missing.
|
125 | * - `guess` will try to guess the string.
|
126 | * - `error` will raise an exception whenever a translation is not defined.
|
127 | *
|
128 | * See {@link MissingTranslation.register} for instructions on how to define
|
129 | * your own behavior.
|
130 | *
|
131 | * @type {MissingBehavior}
|
132 | */
|
133 | public missingBehavior: string;
|
134 |
|
135 | /**
|
136 | * Return a missing placeholder message for given parameters.
|
137 | *
|
138 | * @type {MissingPlaceholderHandler}
|
139 | */
|
140 | public missingPlaceholder: MissingPlaceholderHandler;
|
141 |
|
142 | /**
|
143 | * If you use missingBehavior with 'message', but want to know that the string
|
144 | * is actually missing for testing purposes, you can prefix the guessed string
|
145 | * by setting the value here. By default, no prefix is used.
|
146 | *
|
147 | * @type {string}
|
148 | */
|
149 | public missingTranslationPrefix: string;
|
150 |
|
151 | /**
|
152 | * Return a placeholder message for null values. Defaults to the same behavior
|
153 | * as `I18n.missingPlaceholder`.
|
154 | *
|
155 | * @type {NullPlaceholderHandler}
|
156 | */
|
157 | public nullPlaceholder: NullPlaceholderHandler;
|
158 |
|
159 | /**
|
160 | * The missing translation behavior registry.
|
161 | *
|
162 | * @see {@link MissingTranslation}
|
163 | *
|
164 | * @type {MissingTranslation}
|
165 | */
|
166 | public missingTranslation: MissingTranslation;
|
167 |
|
168 | /**
|
169 | * Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
|
170 | *
|
171 | * @type {RegExp}
|
172 | */
|
173 | public placeholder: RegExp;
|
174 |
|
175 | /**
|
176 | * Set the registered translations. The root key must always be the locale
|
177 | * (and its variations with region).
|
178 | *
|
179 | * Remember that no events will be triggered if you change this object
|
180 | * directly. To trigger `onchange` events, you must perform updates either
|
181 | * using `I18n#store` or `I18n#update`.
|
182 | *
|
183 | * @type {Dict}
|
184 | */
|
185 | public translations: Dict = {};
|
186 |
|
187 | /**
|
188 | * Transform keys. By default, it returns the key as it is, but allows for
|
189 | * overriding. For instance, you can set a function to receive the camelcase
|
190 | * key, and convert it to snake case.
|
191 | *
|
192 | * @type {(key: string) => string}
|
193 | */
|
194 | public transformKey: (key: string) => string;
|
195 |
|
196 | /**
|
197 | * Override the interpolation function. For the default implementation, see
|
198 | * <https://github.com/fnando/i18n/tree/main/src/helpers/interpolate.ts>
|
199 | * @type {(i18n: I18n, message: string, options: TranslateOptions) => string}
|
200 | */
|
201 | public interpolate: typeof interpolate;
|
202 |
|
203 | constructor(translations: Dict = {}, options: Partial<I18nOptions> = {}) {
|
204 | const {
|
205 | locale,
|
206 | enableFallback,
|
207 | missingBehavior,
|
208 | missingTranslationPrefix,
|
209 | missingPlaceholder,
|
210 | nullPlaceholder,
|
211 | defaultLocale,
|
212 | defaultSeparator,
|
213 | placeholder,
|
214 | transformKey,
|
215 | }: I18nOptions = {
|
216 | ...DEFAULT_I18N_OPTIONS,
|
217 | ...options,
|
218 | };
|
219 |
|
220 | this.locale = locale;
|
221 | this.defaultLocale = defaultLocale;
|
222 | this.defaultSeparator = defaultSeparator;
|
223 | this.enableFallback = enableFallback;
|
224 | this.locale = locale;
|
225 | this.missingBehavior = missingBehavior;
|
226 | this.missingTranslationPrefix = missingTranslationPrefix;
|
227 | this.missingPlaceholder = missingPlaceholder;
|
228 | this.nullPlaceholder = nullPlaceholder;
|
229 | this.placeholder = placeholder;
|
230 | this.pluralization = new Pluralization(this);
|
231 | this.locales = new Locales(this);
|
232 | this.missingTranslation = new MissingTranslation(this);
|
233 | this.transformKey = transformKey;
|
234 | this.interpolate = interpolate;
|
235 |
|
236 | this.store(translations);
|
237 | }
|
238 |
|
239 | /**
|
240 | * Update translations by merging them. Newest translations will override
|
241 | * existing ones.
|
242 | *
|
243 | * @param {Dict} translations An object containing the translations that will
|
244 | * be merged into existing translations.
|
245 | *
|
246 | * @returns {void}
|
247 | */
|
248 | public store(translations: Dict): void {
|
249 | merge(this.translations, translations);
|
250 | this.hasChanged();
|
251 | }
|
252 |
|
253 | /**
|
254 | * Return the current locale, using a explicit locale set using
|
255 | * `i18n.locale = newLocale`, the default locale set using
|
256 | * `i18n.defaultLocale` or the fallback, which is `en`.
|
257 | *
|
258 | * @returns {string} The current locale.
|
259 | */
|
260 | public get locale(): string {
|
261 | return this._locale || this.defaultLocale || "en";
|
262 | }
|
263 |
|
264 | /**
|
265 | * Set the current locale explicitly.
|
266 | *
|
267 | * @param {string} newLocale The new locale.
|
268 | */
|
269 | public set locale(newLocale: string) {
|
270 | if (typeof newLocale !== "string") {
|
271 | throw new Error(
|
272 | `Expected newLocale to be a string; got ${inferType(newLocale)}`,
|
273 | );
|
274 | }
|
275 |
|
276 | const changed = this._locale !== newLocale;
|
277 |
|
278 | this._locale = newLocale;
|
279 |
|
280 | if (changed) {
|
281 | this.hasChanged();
|
282 | }
|
283 | }
|
284 |
|
285 | /**
|
286 | * Return the default locale, using a explicit locale set using
|
287 | * `i18n.defaultLocale = locale`, the default locale set using
|
288 | * `i18n.defaultLocale` or the fallback, which is `en`.
|
289 | *
|
290 | * @returns {string} The current locale.
|
291 | */
|
292 | public get defaultLocale(): string {
|
293 | return this._defaultLocale || "en";
|
294 | }
|
295 |
|
296 | /**
|
297 | * Set the default locale explicitly.
|
298 | *
|
299 | * @param {string} newLocale The new locale.
|
300 | */
|
301 | public set defaultLocale(newLocale: string) {
|
302 | if (typeof newLocale !== "string") {
|
303 | throw new Error(
|
304 | `Expected newLocale to be a string; got ${inferType(newLocale)}`,
|
305 | );
|
306 | }
|
307 |
|
308 | const changed = this._defaultLocale !== newLocale;
|
309 |
|
310 | this._defaultLocale = newLocale;
|
311 |
|
312 | if (changed) {
|
313 | this.hasChanged();
|
314 | }
|
315 | }
|
316 |
|
317 | /**
|
318 | * Translate the given scope with the provided options.
|
319 | *
|
320 | * @param {string|array} scope The scope that will be used.
|
321 | *
|
322 | * @param {TranslateOptions} options The options that will be used on the
|
323 | * translation. Can include some special options like `defaultValue`, `count`,
|
324 | * and `scope`. Everything else will be treated as replacement values.
|
325 | *
|
326 | * @param {number} options.count Enable pluralization. The returned
|
327 | * translation will depend on the detected pluralizer.
|
328 | *
|
329 | * @param {any} options.defaultValue The default value that will used in case
|
330 | * the translation defined by `scope` cannot be found. Can be a function that
|
331 | * returns a string; the signature is
|
332 | * `(i18n:I18n, options: TranslateOptions): string`.
|
333 | *
|
334 | * @param {MissingBehavior|string} options.missingBehavior The missing
|
335 | * behavior that will be used instead of the default one.
|
336 | *
|
337 | * @param {Dict[]} options.defaults An array of hashs where the key is the
|
338 | * type of translation desired, a `scope` or a `message`. The translation
|
339 | * returned will be either the first scope recognized, or the first message
|
340 | * defined.
|
341 | *
|
342 | * @returns {T | string} The translated string.
|
343 | */
|
344 | public translate<T = string>(
|
345 | scope: Scope,
|
346 | options?: TranslateOptions,
|
347 | ): string | T {
|
348 | options = { ...options };
|
349 |
|
350 | const translationOptions: TranslateOptions[] = createTranslationOptions(
|
351 | this,
|
352 | scope,
|
353 | options,
|
354 | ) as TranslateOptions[];
|
355 |
|
356 | let translation: string | Dict | undefined;
|
357 |
|
358 | // Iterate through the translation options until a translation
|
359 | // or message is found.
|
360 | const hasFoundTranslation = translationOptions.some(
|
361 | (translationOption: TranslateOptions) => {
|
362 | if (isSet(translationOption.scope)) {
|
363 | translation = lookup(this, translationOption.scope as Scope, options);
|
364 | } else if (isSet(translationOption.message)) {
|
365 | translation = translationOption.message;
|
366 | }
|
367 |
|
368 | return translation !== undefined && translation !== null;
|
369 | },
|
370 | );
|
371 |
|
372 | if (!hasFoundTranslation) {
|
373 | return this.missingTranslation.get(scope, options);
|
374 | }
|
375 |
|
376 | if (typeof translation === "string") {
|
377 | translation = this.interpolate(this, translation, options);
|
378 | } else if (
|
379 | typeof translation === "object" &&
|
380 | translation &&
|
381 | isSet(options.count)
|
382 | ) {
|
383 | translation = pluralize({
|
384 | i18n: this,
|
385 | count: options.count || 0,
|
386 | scope: translation as unknown as string,
|
387 | options,
|
388 | baseScope: getFullScope(this, scope, options),
|
389 | });
|
390 | }
|
391 |
|
392 | if (options && translation instanceof Array) {
|
393 | translation = translation.map((entry) =>
|
394 | typeof entry === "string"
|
395 | ? interpolate(this, entry, options as TranslateOptions)
|
396 | : entry,
|
397 | );
|
398 | }
|
399 |
|
400 | return translation as string | T;
|
401 | }
|
402 |
|
403 | /**
|
404 | * @alias {@link translate}
|
405 | */
|
406 | public t = this.translate;
|
407 |
|
408 | /**
|
409 | * Pluralize the given scope using the `count` value. The pluralized
|
410 | * translation may have other placeholders, which will be retrieved from
|
411 | * `options`.
|
412 | *
|
413 | * @param {number} count The counting number.
|
414 | *
|
415 | * @param {Scope} scope The translation scope.
|
416 | *
|
417 | * @param {TranslateOptions} options The translation options.
|
418 | *
|
419 | * @returns {string} The translated string.
|
420 | */
|
421 | public pluralize(
|
422 | count: number,
|
423 | scope: Scope,
|
424 | options?: TranslateOptions,
|
425 | ): string {
|
426 | return pluralize({
|
427 | i18n: this,
|
428 | count,
|
429 | scope,
|
430 | options: { ...options },
|
431 | baseScope: getFullScope(this, scope, options ?? {}),
|
432 | });
|
433 | }
|
434 |
|
435 | /**
|
436 | * @alias {@link pluralize}
|
437 | */
|
438 | public p = this.pluralize;
|
439 |
|
440 | /**
|
441 | * Localize several values.
|
442 | *
|
443 | * You can provide the following scopes: `currency`, `number`, or
|
444 | * `percentage`. If you provide a scope that matches the `/^(date|time)/`
|
445 | * regular expression then the `value` will be converted by using the
|
446 | * `I18n.toTime` function. It will default to the value's `toString` function.
|
447 | *
|
448 | * If value is either `null` or `undefined` then an empty string will be
|
449 | * returned, regardless of what localization type has been used.
|
450 | *
|
451 | * @param {string} type The localization type.
|
452 | *
|
453 | * @param {string|number|Date} value The value that must be localized.
|
454 | *
|
455 | * @param {Dict} options The localization options.
|
456 | *
|
457 | * @returns {string} The localized string.
|
458 | */
|
459 | public localize(
|
460 | type: string,
|
461 | value: string | number | Date | null | undefined,
|
462 | options?: Dict,
|
463 | ): string {
|
464 | options = { ...options };
|
465 |
|
466 | if (value === undefined || value === null) {
|
467 | return "";
|
468 | }
|
469 |
|
470 | switch (type) {
|
471 | case "currency":
|
472 | return this.numberToCurrency(value as number);
|
473 |
|
474 | case "number":
|
475 | return formatNumber(value as number, {
|
476 | delimiter: ",",
|
477 | precision: 3,
|
478 | separator: ".",
|
479 | significant: false,
|
480 | stripInsignificantZeros: false,
|
481 | ...lookup(this, "number.format"),
|
482 | });
|
483 |
|
484 | case "percentage":
|
485 | return this.numberToPercentage(value as number);
|
486 |
|
487 | default: {
|
488 | let localizedValue: string;
|
489 |
|
490 | if (type.match(/^(date|time)/)) {
|
491 | localizedValue = this.toTime(type, value as DateTime);
|
492 | } else {
|
493 | localizedValue = (value as string | number | Date).toString();
|
494 | }
|
495 |
|
496 | return interpolate(this, localizedValue, options);
|
497 | }
|
498 | }
|
499 | }
|
500 |
|
501 | /**
|
502 | * @alias {@link localize}
|
503 | */
|
504 | public l = this.localize;
|
505 |
|
506 | /**
|
507 | * Convert the given dateString into a formatted date.
|
508 | *
|
509 | * @param {scope} scope The formatting scope.
|
510 | *
|
511 | * @param {DateTime} input The string that must be parsed into a Date object.
|
512 | *
|
513 | * @returns {string} The formatted date.
|
514 | */
|
515 | public toTime(scope: Scope, input: DateTime): string {
|
516 | const date = parseDate(input);
|
517 | const format: string = lookup(this, scope);
|
518 |
|
519 | if (date.toString().match(/invalid/i)) {
|
520 | return date.toString();
|
521 | }
|
522 |
|
523 | if (!format) {
|
524 | return date.toString();
|
525 | }
|
526 |
|
527 | return this.strftime(date, format);
|
528 | }
|
529 |
|
530 | /**
|
531 | * Formats a `number` into a currency string (e.g., $13.65). You can customize
|
532 | * the format in the using an `options` object.
|
533 | *
|
534 | * The currency unit and number formatting of the current locale will be used
|
535 | * unless otherwise specified in the provided options. No currency conversion
|
536 | * is performed. If the user is given a way to change their locale, they will
|
537 | * also be able to change the relative value of the currency displayed with
|
538 | * this helper.
|
539 | *
|
540 | * @example
|
541 | * ```js
|
542 | * i18n.numberToCurrency(1234567890.5);
|
543 | * // => "$1,234,567,890.50"
|
544 | *
|
545 | * i18n.numberToCurrency(1234567890.506);
|
546 | * // => "$1,234,567,890.51"
|
547 | *
|
548 | * i18n.numberToCurrency(1234567890.506, { precision: 3 });
|
549 | * // => "$1,234,567,890.506"
|
550 | *
|
551 | * i18n.numberToCurrency("123a456");
|
552 | * // => "$123a456"
|
553 | *
|
554 | * i18n.numberToCurrency("123a456", { raise: true });
|
555 | * // => raises exception ("123a456" is not a valid numeric value)
|
556 | *
|
557 | * i18n.numberToCurrency(-0.456789, { precision: 0 });
|
558 | * // => "$0"
|
559 | *
|
560 | * i18n.numberToCurrency(-1234567890.5, { negativeFormat: "(%u%n)" });
|
561 | * // => "($1,234,567,890.50)"
|
562 | *
|
563 | * i18n.numberToCurrency(1234567890.5, {
|
564 | * unit: "£",
|
565 | * separator: ",",
|
566 | * delimiter: "",
|
567 | * });
|
568 | * // => "£1234567890,50"
|
569 | *
|
570 | * i18n.numberToCurrency(1234567890.5, {
|
571 | * unit: "£",
|
572 | * separator: ",",
|
573 | * delimiter: "",
|
574 | * format: "%n %u",
|
575 | * });
|
576 | * // => "1234567890,50 £"
|
577 | *
|
578 | * i18n.numberToCurrency(1234567890.5, { stripInsignificantZeros: true });
|
579 | * // => "$1,234,567,890.5"
|
580 | *
|
581 | * i18n.numberToCurrency(1234567890.5, { precision: 0, roundMode: "up" });
|
582 | * // => "$1,234,567,891"
|
583 | * ```
|
584 | *
|
585 | * @param {Numeric} input The number to be formatted.
|
586 | *
|
587 | * @param {NumberToCurrencyOptions} options The formatting options. When
|
588 | * defined, supersedes the default options defined by `number.format` and
|
589 | * `number.currency.*`.
|
590 | *
|
591 | * @param {number} options.precision Sets the level of precision (defaults to
|
592 | * 2).
|
593 | *
|
594 | * @param {RoundingMode} options.roundMode Determine how rounding is performed
|
595 | * (defaults to `default`.)
|
596 | *
|
597 | * @param {string} options.unit Sets the denomination of the currency
|
598 | * (defaults to "$").
|
599 | *
|
600 | * @param {string} options.separator Sets the separator between the units
|
601 | * (defaults to ".").
|
602 | *
|
603 | * @param {string} options.delimiter Sets the thousands delimiter
|
604 | * (defaults to ",").
|
605 | *
|
606 | * @param {string} options.format Sets the format for non-negative numbers
|
607 | * (defaults to "%u%n"). Fields are `%u` for the currency, and `%n` for the
|
608 | * number.
|
609 | *
|
610 | * @param {string} options.negativeFormat Sets the format for negative numbers
|
611 | * (defaults to prepending a hyphen to the formatted number given by
|
612 | * `format`). Accepts the same fields than `format`, except `%n` is here the
|
613 | * absolute value of the number.
|
614 | *
|
615 | * @param {boolean} options.stripInsignificantZeros If `true` removes
|
616 | * insignificant zeros after the decimal separator (defaults to `false`).
|
617 | *
|
618 | * @param {boolean} options.raise If `true`, raises exception for non-numeric
|
619 | * values like `NaN` and infinite values.
|
620 | *
|
621 | * @returns {string} The formatted number.
|
622 | */
|
623 | public numberToCurrency(
|
624 | input: Numeric,
|
625 | options: Partial<NumberToCurrencyOptions> = {},
|
626 | ): string {
|
627 | return formatNumber(input, {
|
628 | delimiter: ",",
|
629 | format: "%u%n",
|
630 | precision: 2,
|
631 | separator: ".",
|
632 | significant: false,
|
633 | stripInsignificantZeros: false,
|
634 | unit: "$",
|
635 | ...camelCaseKeys<Partial<FormatNumberOptions>>(this.get("number.format")),
|
636 | ...camelCaseKeys<Partial<NumberToCurrencyOptions>>(
|
637 | this.get("number.currency.format"),
|
638 | ),
|
639 | ...options,
|
640 | } as FormatNumberOptions);
|
641 | }
|
642 |
|
643 | /**
|
644 | * Convert a number into a formatted percentage value.
|
645 | *
|
646 | * @example
|
647 | * ```js
|
648 | * i18n.numberToPercentage(100);
|
649 | * // => "100.000%"
|
650 | *
|
651 | * i18n.numberToPercentage("98");
|
652 | * // => "98.000%"
|
653 | *
|
654 | * i18n.numberToPercentage(100, { precision: 0 });
|
655 | * // => "100%"
|
656 | *
|
657 | * i18n.numberToPercentage(1000, { delimiter: ".", separator: "," });
|
658 | * // => "1.000,000%"
|
659 | *
|
660 | * i18n.numberToPercentage(302.24398923423, { precision: 5 });
|
661 | * // => "302.24399%"
|
662 | *
|
663 | * i18n.numberToPercentage(1000, { precision: null });
|
664 | * // => "1000%"
|
665 | *
|
666 | * i18n.numberToPercentage("98a");
|
667 | * // => "98a%"
|
668 | *
|
669 | * i18n.numberToPercentage(100, { format: "%n %" });
|
670 | * // => "100.000 %"
|
671 | *
|
672 | * i18n.numberToPercentage(302.24398923423, { precision: 5, roundMode: "down" });
|
673 | * // => "302.24398%"
|
674 | * ```
|
675 | *
|
676 | * @param {Numeric} input The number to be formatted.
|
677 | *
|
678 | * @param {NumberToPercentageOptions} options The formatting options. When
|
679 | * defined, supersedes the default options stored at `number.format` and
|
680 | * `number.percentage.*`.
|
681 | *
|
682 | * @param {number} options.precision Sets the level of precision (defaults to
|
683 | * 3).
|
684 | *
|
685 | * @param {RoundingMode} options.roundMode Determine how rounding is performed
|
686 | * (defaults to `default`.)
|
687 | *
|
688 | * @param {string} options.separator Sets the separator between the units
|
689 | * (defaults to ".").
|
690 | *
|
691 | * @param {string} options.delimiter Sets the thousands delimiter (defaults to
|
692 | * "").
|
693 | *
|
694 | * @param {string} options.format Sets the format for non-negative numbers
|
695 | * (defaults to "%n%"). The number field is represented by `%n`.
|
696 | *
|
697 | * @param {string} options.negativeFormat Sets the format for negative numbers
|
698 | * (defaults to prepending a hyphen to the formatted number given by
|
699 | * `format`). Accepts the same fields than `format`, except `%n` is here the
|
700 | * absolute value of the number.
|
701 | *
|
702 | * @param {boolean} options.stripInsignificantZeros If `true` removes
|
703 | * insignificant zeros after the decimal separator (defaults to `false`).
|
704 | *
|
705 | * @returns {string} The formatted number.
|
706 | */
|
707 | public numberToPercentage(
|
708 | input: Numeric,
|
709 | options: Partial<NumberToPercentageOptions> = {},
|
710 | ): string {
|
711 | return formatNumber(input, {
|
712 | delimiter: "",
|
713 | format: "%n%",
|
714 | precision: 3,
|
715 | stripInsignificantZeros: false,
|
716 | separator: ".",
|
717 | significant: false,
|
718 | ...camelCaseKeys<Partial<FormatNumberOptions>>(this.get("number.format")),
|
719 | ...camelCaseKeys<Partial<NumberToPercentageOptions>>(
|
720 | this.get("number.percentage.format"),
|
721 | ),
|
722 | ...options,
|
723 | } as FormatNumberOptions);
|
724 | }
|
725 |
|
726 | /**
|
727 | * Convert a number into a readable size representation.
|
728 | *
|
729 | * @example
|
730 | * ```js
|
731 | * i18n.numberToHumanSize(123)
|
732 | * // => "123 Bytes"
|
733 | *
|
734 | * i18n.numberToHumanSize(1234)
|
735 | * // => "1.21 KB"
|
736 | *
|
737 | * i18n.numberToHumanSize(12345)
|
738 | * // => "12.1 KB"
|
739 | *
|
740 | * i18n.numberToHumanSize(1234567)
|
741 | * // => "1.18 MB"
|
742 | *
|
743 | * i18n.numberToHumanSize(1234567890)
|
744 | * // => "1.15 GB"
|
745 | *
|
746 | * i18n.numberToHumanSize(1234567890123)
|
747 | * // => "1.12 TB"
|
748 | *
|
749 | * i18n.numberToHumanSize(1234567890123456)
|
750 | * // => "1.1 PB"
|
751 | *
|
752 | * i18n.numberToHumanSize(1234567890123456789)
|
753 | * // => "1.07 EB"
|
754 | *
|
755 | * i18n.numberToHumanSize(1234567, {precision: 2})
|
756 | * // => "1.2 MB"
|
757 | *
|
758 | * i18n.numberToHumanSize(483989, precision: 2)
|
759 | * // => "470 KB"
|
760 | *
|
761 | * i18n.numberToHumanSize(483989, {precision: 2, roundMode: "up"})
|
762 | * // => "480 KB"
|
763 | *
|
764 | * i18n.numberToHumanSize(1234567, {precision: 2, separator: ","})
|
765 | * // => "1,2 MB"
|
766 | *
|
767 | * i18n.numberToHumanSize(1234567890123, {precision: 5})
|
768 | * // => "1.1228 TB"
|
769 | *
|
770 | * i18n.numberToHumanSize(524288000, {precision: 5})
|
771 | * // => "500 MB"
|
772 | * ```
|
773 | *
|
774 | * @param {Numeric} input The number that will be formatted.
|
775 | *
|
776 | * @param {NumberToHumanSizeOptions} options The formatting options. When
|
777 | * defined, supersedes the default options stored at
|
778 | * `number.human.storage_units.*` and `number.human.format`.
|
779 | *
|
780 | * @param {number} options.precision Sets the precision of the number
|
781 | * (defaults to 3).
|
782 | *
|
783 | * @param {RoundingMode} options.roundMode Determine how rounding is performed
|
784 | * (defaults to `default`)
|
785 | *
|
786 | * @param {boolean} options.significant If `true`, precision will be the
|
787 | * number of significant digits. If `false`, the number of fractional digits
|
788 | * (defaults to `true`).
|
789 | *
|
790 | * @param {string} options.separator Sets the separator between the fractional
|
791 | * and integer digits (defaults to ".").
|
792 | *
|
793 | * @param {string} options.delimiter Sets the thousands delimiter (defaults
|
794 | * to "").
|
795 | *
|
796 | * @param {boolean} options.stripInsignificantZeros If `true` removes
|
797 | * insignificant zeros after the decimal separator (defaults to `true`).
|
798 | *
|
799 | * @returns {string} The formatted number.
|
800 | */
|
801 | public numberToHumanSize(
|
802 | input: Numeric,
|
803 | options: Partial<NumberToHumanSizeOptions> = {},
|
804 | ): string {
|
805 | return numberToHumanSize(this, input, {
|
806 | delimiter: "",
|
807 | precision: 3,
|
808 | significant: true,
|
809 | stripInsignificantZeros: true,
|
810 | units: {
|
811 | billion: "Billion",
|
812 | million: "Million",
|
813 | quadrillion: "Quadrillion",
|
814 | thousand: "Thousand",
|
815 | trillion: "Trillion",
|
816 | unit: "",
|
817 | },
|
818 | ...camelCaseKeys<Partial<NumberToHumanSizeOptions>>(
|
819 | this.get("number.human.format"),
|
820 | ),
|
821 | ...camelCaseKeys<Partial<NumberToHumanSizeOptions>>(
|
822 | this.get("number.human.storage_units"),
|
823 | ),
|
824 | ...options,
|
825 | } as NumberToHumanSizeOptions);
|
826 | }
|
827 |
|
828 | /**
|
829 | * Convert a number into a readable representation.
|
830 | *
|
831 | * @example
|
832 | * ```js
|
833 | * i18n.numberToHuman(123);
|
834 | * // => "123"
|
835 | *
|
836 | * i18n.numberToHuman(1234);
|
837 | * // => "1.23 Thousand"
|
838 | *
|
839 | * i18n.numberToHuman(12345);
|
840 | * // => "12.3 Thousand"
|
841 | *
|
842 | * i18n.numberToHuman(1234567);
|
843 | * // => "1.23 Million"
|
844 | *
|
845 | * i18n.numberToHuman(1234567890);
|
846 | * // => "1.23 Billion"
|
847 | *
|
848 | * i18n.numberToHuman(1234567890123);
|
849 | * // => "1.23 Trillion"
|
850 | *
|
851 | * i18n.numberToHuman(1234567890123456);
|
852 | * // => "1.23 Quadrillion"
|
853 | *
|
854 | * i18n.numberToHuman(1234567890123456789);
|
855 | * // => "1230 Quadrillion"
|
856 | *
|
857 | * i18n.numberToHuman(489939, { precision: 2 });
|
858 | * // => "490 Thousand"
|
859 | *
|
860 | * i18n.numberToHuman(489939, { precision: 4 });
|
861 | * // => "489.9 Thousand"
|
862 | *
|
863 | * i18n.numberToHuman(489939, { precision: 2, roundMode: "down" });
|
864 | * // => "480 Thousand"
|
865 | *
|
866 | * i18n.numberToHuman(1234567, { precision: 4, significant: false });
|
867 | * // => "1.2346 Million"
|
868 | *
|
869 | * i18n.numberToHuman(1234567, {
|
870 | * precision: 1,
|
871 | * separator: ",",
|
872 | * significant: false,
|
873 | * });
|
874 | * // => "1,2 Million"
|
875 | *
|
876 | * i18n.numberToHuman(500000000, { precision: 5 });
|
877 | * // => "500 Million"
|
878 | *
|
879 | * i18n.numberToHuman(12345012345, { significant: false });
|
880 | * // => "12.345 Billion"
|
881 | * ```
|
882 | *
|
883 | * Non-significant zeros after the decimal separator are stripped out by default
|
884 | * (set `stripInsignificantZeros` to `false` to change that):
|
885 | *
|
886 | * ```js
|
887 | * i18n.numberToHuman(12.00001);
|
888 | * // => "12"
|
889 | *
|
890 | * i18n.numberToHuman(12.00001, { stripInsignificantZeros: false });
|
891 | * // => "12.0"
|
892 | * ```
|
893 | *
|
894 | * You can also use your own custom unit quantifiers:
|
895 | *
|
896 | * ```js
|
897 | * i18n.numberToHuman(500000, units: { unit: "ml", thousand: "lt" });
|
898 | * // => "500 lt"
|
899 | * ```
|
900 | *
|
901 | * If in your I18n locale you have:
|
902 | *
|
903 | * ```yaml
|
904 | * ---
|
905 | * en:
|
906 | * distance:
|
907 | * centi:
|
908 | * one: "centimeter"
|
909 | * other: "centimeters"
|
910 | * unit:
|
911 | * one: "meter"
|
912 | * other: "meters"
|
913 | * thousand:
|
914 | * one: "kilometer"
|
915 | * other: "kilometers"
|
916 | * billion: "gazillion-distance"
|
917 | * ```
|
918 | *
|
919 | * Then you could do:
|
920 | *
|
921 | * ```js
|
922 | * i18n.numberToHuman(543934, { units: "distance" });
|
923 | * // => "544 kilometers"
|
924 | *
|
925 | * i18n.numberToHuman(54393498, { units: "distance" });
|
926 | * // => "54400 kilometers"
|
927 | *
|
928 | * i18n.numberToHuman(54393498000, { units: "distance" });
|
929 | * // => "54.4 gazillion-distance"
|
930 | *
|
931 | * i18n.numberToHuman(343, { units: "distance", precision: 1 });
|
932 | * // => "300 meters"
|
933 | *
|
934 | * i18n.numberToHuman(1, { units: "distance" });
|
935 | * // => "1 meter"
|
936 | *
|
937 | * i18n.numberToHuman(0.34, { units: "distance" });
|
938 | * // => "34 centimeters"
|
939 | * ```
|
940 | *
|
941 | * @param {Numeric} input The number that will be formatted.
|
942 | *
|
943 | * @param {NumberToHumanOptions} options The formatting options. When
|
944 | * defined, supersedes the default options stored at `number.human.format.*`
|
945 | * and `number.human.storage_units.*`.
|
946 | *
|
947 | * @param {number} options.precision Sets the precision of the number
|
948 | * (defaults to 3).
|
949 | *
|
950 | * @param {RoundingMode} options.roundMode Determine how rounding is performed
|
951 | * (defaults to `default`).
|
952 | *
|
953 | * @param {boolean} options.significant If `true`, precision will be the
|
954 | * number of significant_digits. If `false`, the number of fractional digits
|
955 | * (defaults to `true`)
|
956 | *
|
957 | * @param {string} options.separator Sets the separator between the fractional
|
958 | * and integer digits (defaults to ".").
|
959 | *
|
960 | * @param {string} options.delimiter Sets the thousands delimiter
|
961 | * (defaults to "").
|
962 | *
|
963 | * @param {boolean} options.stripInsignificantZeros If `true` removes
|
964 | * insignificant zeros after the decimal separator (defaults to `true`).
|
965 | *
|
966 | * @param {Dict} options.units A Hash of unit quantifier names. Or a string
|
967 | * containing an I18n scope where to find this object. It might have the
|
968 | * following keys:
|
969 | *
|
970 | * - _integers_: `unit`, `ten`, `hundred`, `thousand`, `million`, `billion`,
|
971 | * `trillion`, `quadrillion`
|
972 | * - _fractionals_: `deci`, `centi`, `mili`, `micro`, `nano`, `pico`, `femto`
|
973 | *
|
974 | * @param {string} options.format Sets the format of the output string
|
975 | * (defaults to "%n %u"). The field types are:
|
976 | *
|
977 | * - `%u` - The quantifier (ex.: 'thousand')
|
978 | * - `%n` - The number
|
979 | *
|
980 | * @returns {string} The formatted number.
|
981 | */
|
982 | public numberToHuman(
|
983 | input: Numeric,
|
984 | options: Partial<NumberToHumanOptions> = {},
|
985 | ): string {
|
986 | return numberToHuman(this, input, {
|
987 | delimiter: "",
|
988 | separator: ".",
|
989 | precision: 3,
|
990 | significant: true,
|
991 | stripInsignificantZeros: true,
|
992 | format: "%n %u",
|
993 | roundMode: "default",
|
994 | units: {
|
995 | billion: "Billion",
|
996 | million: "Million",
|
997 | quadrillion: "Quadrillion",
|
998 | thousand: "Thousand",
|
999 | trillion: "Trillion",
|
1000 | unit: "",
|
1001 | },
|
1002 | ...camelCaseKeys<Partial<NumberToHumanOptions>>(
|
1003 | this.get("number.human.format"),
|
1004 | ),
|
1005 | ...camelCaseKeys<Partial<NumberToHumanOptions>>(
|
1006 | this.get("number.human.decimal_units"),
|
1007 | ),
|
1008 | ...options,
|
1009 | } as NumberToHumanOptions);
|
1010 | }
|
1011 |
|
1012 | /**
|
1013 | * Convert number to a formatted rounded value.
|
1014 | *
|
1015 | * @example
|
1016 | * ```js
|
1017 | * i18n.numberToRounded(111.2345);
|
1018 | * // => "111.235"
|
1019 | *
|
1020 | * i18n.numberToRounded(111.2345, { precision: 2 });
|
1021 | * // => "111.23"
|
1022 | *
|
1023 | * i18n.numberToRounded(13, { precision: 5 });
|
1024 | * // => "13.00000"
|
1025 | *
|
1026 | * i18n.numberToRounded(389.32314, { precision: 0 });
|
1027 | * // => "389"
|
1028 | *
|
1029 | * i18n.numberToRounded(111.2345, { significant: true });
|
1030 | * // => "111"
|
1031 | *
|
1032 | * i18n.numberToRounded(111.2345, { precision: 1, significant: true });
|
1033 | * // => "100"
|
1034 | *
|
1035 | * i18n.numberToRounded(13, { precision: 5, significant: true });
|
1036 | * // => "13.000"
|
1037 | *
|
1038 | * i18n.numberToRounded(13, { precision: null });
|
1039 | * // => "13"
|
1040 | *
|
1041 | * i18n.numberToRounded(389.32314, { precision: 0, roundMode: "up" });
|
1042 | * // => "390"
|
1043 | *
|
1044 | * i18n.numberToRounded(13, {
|
1045 | * precision: 5,
|
1046 | * significant: true,
|
1047 | * stripInsignificantZeros: true,
|
1048 | * });
|
1049 | * // => "13"
|
1050 | *
|
1051 | * i18n.numberToRounded(389.32314, { precision: 4, significant: true });
|
1052 | * // => "389.3"
|
1053 | *
|
1054 | * i18n.numberToRounded(1111.2345, {
|
1055 | * precision: 2,
|
1056 | * separator: ",",
|
1057 | * delimiter: ".",
|
1058 | * });
|
1059 | * // => "1.111,23"
|
1060 | * ```
|
1061 | *
|
1062 | * @param {Numeric} input The number to be formatted.
|
1063 | *
|
1064 | * @param {NumberToRoundedOptions} options The formatting options.
|
1065 | *
|
1066 | * @param {number} options.precision Sets the precision of the number
|
1067 | * (defaults to 3).
|
1068 | *
|
1069 | * @param {string} options.separator Sets the separator between the
|
1070 | * fractional and integer digits (defaults to ".").
|
1071 | *
|
1072 | * @param {RoundingMode} options.roundMode Determine how rounding is
|
1073 | * performed.
|
1074 | *
|
1075 | * @param {boolean} options.significant If `true`, precision will be the
|
1076 | * number of significant_digits. If `false`, the number of fractional digits
|
1077 | * (defaults to `false`).
|
1078 | *
|
1079 | * @param {boolean} options.stripInsignificantZeros If `true` removes
|
1080 | * insignificant zeros after the decimal separator (defaults to `false`).
|
1081 | *
|
1082 | * @returns {string} The formatted number.
|
1083 | */
|
1084 | public numberToRounded(
|
1085 | input: Numeric,
|
1086 | options?: Partial<NumberToRoundedOptions>,
|
1087 | ): string {
|
1088 | return formatNumber(input, {
|
1089 | unit: "",
|
1090 | precision: 3,
|
1091 | significant: false,
|
1092 | separator: ".",
|
1093 | delimiter: "",
|
1094 | stripInsignificantZeros: false,
|
1095 | ...options,
|
1096 | } as FormatNumberOptions);
|
1097 | }
|
1098 |
|
1099 | /**
|
1100 | * Formats a +number+ with grouped thousands using `delimiter` (e.g., 12,324).
|
1101 | * You can customize the format in the `options` parameter.
|
1102 | *
|
1103 | * @example
|
1104 | * ```js
|
1105 | * i18n.numberToDelimited(12345678);
|
1106 | * // => "12,345,678"
|
1107 | *
|
1108 | * i18n.numberToDelimited("123456");
|
1109 | * // => "123,456"
|
1110 | *
|
1111 | * i18n.numberToDelimited(12345678.05);
|
1112 | * // => "12,345,678.05"
|
1113 | *
|
1114 | * i18n.numberToDelimited(12345678, { delimiter: "." });
|
1115 | * // => "12.345.678"
|
1116 | *
|
1117 | * i18n.numberToDelimited(12345678, { delimiter: "," });
|
1118 | * // => "12,345,678"
|
1119 | *
|
1120 | * i18n.numberToDelimited(12345678.05, { separator: " " });
|
1121 | * // => "12,345,678 05"
|
1122 | *
|
1123 | * i18n.numberToDelimited("112a");
|
1124 | * // => "112a"
|
1125 | *
|
1126 | * i18n.numberToDelimited(98765432.98, { delimiter: " ", separator: "," });
|
1127 | * // => "98 765 432,98"
|
1128 | *
|
1129 | * i18n.numberToDelimited("123456.78", {
|
1130 | * delimiterPattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/g,
|
1131 | * });
|
1132 | * // => "1,23,456.78"
|
1133 | * ```
|
1134 | *
|
1135 | * @param {Numeric} input The numeric value that will be formatted.
|
1136 | *
|
1137 | * @param {NumberToDelimitedOptions} options The formatting options.
|
1138 | *
|
1139 | * @param {string} options.delimiter Sets the thousands delimiter (defaults to
|
1140 | * ",").
|
1141 | *
|
1142 | * @param {string} options.separator Sets the separator between the fractional
|
1143 | * and integer digits (defaults to ".").
|
1144 | *
|
1145 | * @param {RegExp} options.delimiterPattern Sets a custom regular expression
|
1146 | * used for deriving the placement of delimiter. Helpful when using currency
|
1147 | * formats like INR.
|
1148 | *
|
1149 | * @return {string} The formatted number.
|
1150 | */
|
1151 | public numberToDelimited(
|
1152 | input: Numeric,
|
1153 | options: Partial<NumberToDelimitedOptions> = {},
|
1154 | ): string {
|
1155 | return numberToDelimited(input, {
|
1156 | delimiterPattern: /(\d)(?=(\d\d\d)+(?!\d))/g,
|
1157 | delimiter: ",",
|
1158 | separator: ".",
|
1159 | ...options,
|
1160 | } as NumberToDelimitedOptions);
|
1161 | }
|
1162 |
|
1163 | /**
|
1164 | * Executes function with given locale set. The locale will be changed only
|
1165 | * during the `callback`'s execution, switching back to the previous value
|
1166 | * once it finishes (with or without errors).
|
1167 | *
|
1168 | * This is an asynchronous call, which means you must use `await` or you may
|
1169 | * end up with a race condition.
|
1170 | *
|
1171 | * @example
|
1172 | * ```js
|
1173 | * await i18n.withLocale("pt", () => {
|
1174 | * console.log(i18n.t("hello"));
|
1175 | * });
|
1176 | * ```
|
1177 | *
|
1178 | * @param {string} locale The temporary locale that will be set during the
|
1179 | * function's execution.
|
1180 | *
|
1181 | * @param {Function} callback The function that will be executed with a
|
1182 | * temporary locale set.
|
1183 | *
|
1184 | * @returns {void}
|
1185 | */
|
1186 | public async withLocale(locale: string, callback: () => void): Promise<void> {
|
1187 | const originalLocale = this.locale;
|
1188 |
|
1189 | try {
|
1190 | this.locale = locale;
|
1191 | await callback();
|
1192 | } finally {
|
1193 | this.locale = originalLocale;
|
1194 | }
|
1195 | }
|
1196 |
|
1197 | /**
|
1198 | * Formats time according to the directives in the given format string.
|
1199 | * The directives begins with a percent (`%`) character. Any text not listed
|
1200 | * as a directive will be passed through to the output string.
|
1201 | *
|
1202 | * @see strftime
|
1203 | *
|
1204 | * @param {Date} date The date that will be formatted.
|
1205 | *
|
1206 | * @param {string} format The formatting string.
|
1207 | *
|
1208 | * @param {StrftimeOptions} options The formatting options.
|
1209 | *
|
1210 | * @returns {string} The formatted date.
|
1211 | */
|
1212 | public strftime(
|
1213 | date: Date,
|
1214 | format: string,
|
1215 | options: Partial<StrftimeOptions> = {},
|
1216 | ): string {
|
1217 | return strftime(date, format, {
|
1218 | ...camelCaseKeys(lookup(this, "date")),
|
1219 | meridian: {
|
1220 | am: lookup(this, "time.am") || "AM",
|
1221 | pm: lookup(this, "time.pm") || "PM",
|
1222 | },
|
1223 | ...options,
|
1224 | });
|
1225 | }
|
1226 |
|
1227 | /**
|
1228 | * You may want to update a part of your translations. This is a public
|
1229 | * interface for doing it so.
|
1230 | *
|
1231 | * If the provided path exists, it'll be replaced. Otherwise, a new node will
|
1232 | * be created. When running in strict mode, paths that doesn't already exist
|
1233 | * will raise an exception.
|
1234 | *
|
1235 | * Strict mode will also raise an exception if the override type differs from
|
1236 | * previous node type.
|
1237 | *
|
1238 | * @example
|
1239 | * ```js
|
1240 | * i18n.update("en.number.format", {unit: "%n %u"});
|
1241 | * i18n.update("en.number.format", {unit: "%n %u"}, true);
|
1242 | * ```
|
1243 | *
|
1244 | * @param {string} path The path that's going to be updated. It must
|
1245 | * include the language, as in `en.messages`.
|
1246 | *
|
1247 | * @param {Dict} override The new translation node.
|
1248 | *
|
1249 | * @param {boolean} options Set options.
|
1250 | *
|
1251 | * @param {boolean} options.strict Raise an exception if path doesn't already
|
1252 | * exist, or if previous node's type differs from new node's type.
|
1253 | *
|
1254 | * @returns {void}
|
1255 | */
|
1256 | public update(
|
1257 | path: string,
|
1258 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
1259 | override: any,
|
1260 | options: { strict: boolean } = { strict: false },
|
1261 | ): void {
|
1262 | if (options.strict && !has(this.translations, path)) {
|
1263 | throw new Error(`The path "${path}" is not currently defined`);
|
1264 | }
|
1265 |
|
1266 | const currentNode = get(this.translations, path);
|
1267 | const currentType = inferType(currentNode);
|
1268 | const overrideType = inferType(override);
|
1269 |
|
1270 | if (options.strict && currentType !== overrideType) {
|
1271 | throw new Error(
|
1272 | `The current type for "${path}" is "${currentType}", but you're trying to override it with "${overrideType}"`,
|
1273 | );
|
1274 | }
|
1275 |
|
1276 | let newNode: unknown;
|
1277 |
|
1278 | if (overrideType === "object") {
|
1279 | newNode = { ...currentNode, ...override };
|
1280 | } else {
|
1281 | newNode = override;
|
1282 | }
|
1283 |
|
1284 | const components = path.split(this.defaultSeparator);
|
1285 | const prop = components.pop();
|
1286 | let buffer = this.translations;
|
1287 |
|
1288 | for (const component of components) {
|
1289 | if (!buffer[component]) {
|
1290 | buffer[component] = {};
|
1291 | }
|
1292 |
|
1293 | buffer = buffer[component];
|
1294 | }
|
1295 |
|
1296 | buffer[prop as keyof typeof buffer] = newNode;
|
1297 |
|
1298 | this.hasChanged();
|
1299 | }
|
1300 |
|
1301 | /**
|
1302 | * Converts the array to a comma-separated sentence where the last element is
|
1303 | * joined by the connector word.
|
1304 | *
|
1305 | * @example
|
1306 | * ```js
|
1307 | * i18n.toSentence(["apple", "banana", "pineapple"]);
|
1308 | * //=> apple, banana, and pineapple.
|
1309 | * ```
|
1310 | *
|
1311 | * @param {any[]} items The list of items that will be joined.
|
1312 | *
|
1313 | * @param {ToSentenceOptions} options The options.
|
1314 | *
|
1315 | * @param {string} options.wordsConnector The sign or word used to join the
|
1316 | * elements in arrays with two or more elements (default: ", ").
|
1317 | *
|
1318 | * @param {string} options.twoWordsConnector The sign or word used to join the
|
1319 | * elements in arrays with two elements (default: " and ").
|
1320 | *
|
1321 | * @param {string} options.lastWordConnector The sign or word used to join the
|
1322 | * last element in arrays with three or more elements (default: ", and ").
|
1323 | *
|
1324 | * @returns {string} The joined string.
|
1325 | */
|
1326 | public toSentence(
|
1327 | items: any[],
|
1328 | options: Partial<ToSentenceOptions> = {},
|
1329 | ): string {
|
1330 | const { wordsConnector, twoWordsConnector, lastWordConnector } = {
|
1331 | wordsConnector: ", ",
|
1332 | twoWordsConnector: " and ",
|
1333 | lastWordConnector: ", and ",
|
1334 | ...camelCaseKeys<Partial<ToSentenceOptions>>(
|
1335 | lookup(this, "support.array"),
|
1336 | ),
|
1337 | ...options,
|
1338 | } as ToSentenceOptions;
|
1339 |
|
1340 | const size = items.length;
|
1341 |
|
1342 | switch (size) {
|
1343 | case 0:
|
1344 | return "";
|
1345 |
|
1346 | case 1:
|
1347 | return `${items[0]}`;
|
1348 |
|
1349 | case 2:
|
1350 | return items.join(twoWordsConnector);
|
1351 |
|
1352 | default:
|
1353 | return [
|
1354 | items.slice(0, size - 1).join(wordsConnector),
|
1355 | lastWordConnector,
|
1356 | items[size - 1],
|
1357 | ].join("");
|
1358 | }
|
1359 | }
|
1360 |
|
1361 | /**
|
1362 | * Reports the approximate distance in time between two time representations.
|
1363 | *
|
1364 | * @param {DateTime} fromTime The initial time.
|
1365 | *
|
1366 | * @param {DateTime} toTime The ending time. Defaults to `Date.now()`.
|
1367 | *
|
1368 | * @param {TimeAgoInWordsOptions} options The options.
|
1369 | *
|
1370 | * @param {boolean} options.includeSeconds Pass `{includeSeconds: true}` if
|
1371 | * you want more detailed approximations when distance < 1 min, 29 secs.
|
1372 | *
|
1373 | * @param {Scope} options.scope With the scope option, you can define a custom
|
1374 | * scope to look up the translation.
|
1375 | *
|
1376 | * @returns {string} The distance in time representation.
|
1377 | */
|
1378 | public timeAgoInWords(
|
1379 | fromTime: DateTime,
|
1380 | toTime: DateTime,
|
1381 | options: TimeAgoInWordsOptions = {},
|
1382 | ): string {
|
1383 | return timeAgoInWords(this, fromTime, toTime, options);
|
1384 | }
|
1385 |
|
1386 | /**
|
1387 | * @alias {@link timeAgoInWords}
|
1388 | */
|
1389 | public distanceOfTimeInWords = this.timeAgoInWords;
|
1390 |
|
1391 | /**
|
1392 | * Add a callback that will be executed whenever locale/defaultLocale changes,
|
1393 | * or `I18n#store` / `I18n#update` is called.
|
1394 | *
|
1395 | * @param {OnChangeHandler} callback The callback that will be executed.
|
1396 | *
|
1397 | * @returns {Function} Return a function that can be used to unsubscribe the
|
1398 | * event handler.
|
1399 | *
|
1400 | */
|
1401 | public onChange(callback: OnChangeHandler): () => void {
|
1402 | this.onChangeHandlers.push(callback);
|
1403 |
|
1404 | return () => {
|
1405 | this.onChangeHandlers.splice(this.onChangeHandlers.indexOf(callback), 1);
|
1406 | };
|
1407 | }
|
1408 |
|
1409 | /**
|
1410 | * Return the change version. This value is incremented whenever `I18n#store`
|
1411 | * or `I18n#update` is called, or when `I18n#locale`/`I18n#defaultLocale` is
|
1412 | * set.
|
1413 | */
|
1414 | public get version(): number {
|
1415 | return this._version;
|
1416 | }
|
1417 |
|
1418 | /**
|
1419 | * Formats a number.
|
1420 | *
|
1421 | * @param {Numeric} input The numeric value that will be
|
1422 | * formatted.
|
1423 | * @param {FormatNumberOptions} options The formatting options. Defaults to:
|
1424 | * `{
|
1425 | * delimiter: ",",
|
1426 | * precision: 3,
|
1427 | * separator: ".",
|
1428 | * unit: "",
|
1429 | * format: "%u%n",
|
1430 | * significant: false,
|
1431 | * stripInsignificantZeros: false,
|
1432 | * }`
|
1433 | * @return {string} The formatted number.
|
1434 | */
|
1435 | public formatNumber(
|
1436 | input: Numeric,
|
1437 | options: Partial<FormatNumberOptions> = {},
|
1438 | ): string {
|
1439 | options = {
|
1440 | delimiter: ",",
|
1441 | precision: 3,
|
1442 | separator: ".",
|
1443 | unit: "",
|
1444 | format: "%u%n",
|
1445 | significant: false,
|
1446 | stripInsignificantZeros: false,
|
1447 | ...camelCaseKeys<Partial<FormatNumberOptions>>(this.get("number.format")),
|
1448 | ...options,
|
1449 | };
|
1450 |
|
1451 | return formatNumber(input, options as FormatNumberOptions);
|
1452 | }
|
1453 |
|
1454 | /**
|
1455 | * @param {Scope} scope The scope lookup path.
|
1456 | *
|
1457 | * @returns {any} The found scope.
|
1458 | */
|
1459 | public get(scope: Scope): any {
|
1460 | return lookup(this, scope);
|
1461 | }
|
1462 |
|
1463 | /**
|
1464 | * @private
|
1465 | *
|
1466 | * @returns {void}
|
1467 | */
|
1468 | private runCallbacks(): void {
|
1469 | this.onChangeHandlers.forEach((callback) => callback(this));
|
1470 | }
|
1471 |
|
1472 | /**
|
1473 | * @private
|
1474 | *
|
1475 | * @returns {void}
|
1476 | */
|
1477 | private hasChanged(): void {
|
1478 | this._version += 1;
|
1479 |
|
1480 | this.runCallbacks();
|
1481 | }
|
1482 | }
|