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