vue-i18n icon indicating copy to clipboard operation
vue-i18n copied to clipboard

Allow format number with any locale, and not the only described in numberFormats object

Open cooperok opened this issue 2 years ago • 0 comments
trafficstars

Clear and concise description of the problem

In version 8 I was able to format numbers with <i18n-n> component to any locale I specify in locale property. All I had to do is to generate proper options like this:

new Intl.NumberFormat(this.locale, {key: 'currency', style: 'currency'}).resolvedOptions()

in numberFormats object I had only one locale with the 'currency' property.

During migration to version 9 I faced the problem that the number are not formatted. Only when I added additional locales inside numberFormats numbers became formatted. This is not handy since I want to have a universal tool and be able to format numbers even for locales where I don't have any translations. With the minimum numberFormats configuration, like this:

numberFormats: {
en: {
    currency: {
      style: 'currency',
      currencyDisplay: 'symbol',
      minimumFractionDigits: 0,
      maximumFractionDigits: 2
    }
}
}

Suggested solution

I'm not familiar with the JS build tools, but I debugged this file node_modules/vue-i18n/dist/vue-i18n.global.js and found that there is no option to format a number unless you did not specify required locale in numberFormats object.

Little stack trace:
there is a declaration of const NumberFormat, inside its setup method renderFormatter call, one of arguments is i18n[NumberPartsSymbol](...args), which is equivalent for numberParts function. numberParts function has a call of number function. It's purpose to return parts of a number mainly made with Intl.NumberFormat.

So the number function has a loop through locales, and it contains a requested locale and a fallback locale. The problem is that there is a condition to break the loop only if there is a declared locale in numberFormats object.

Here is a snippet (I added a comments with capital letters):

// implementation of `number` function
  function number(context, ...args) {
      const { numberFormats, unresolving, fallbackLocale, onWarn, localeFallbacker } = context;
      const { __numberFormatters } = context;
      if (!Availabilities.numberFormat) {
          onWarn(getWarnMessage$1(CoreWarnCodes.CANNOT_FORMAT_NUMBER));
          return MISSING_RESOLVE_VALUE;
      }
      const [key, value, options, overrides] = parseNumberArgs(...args);
      const missingWarn = isBoolean(options.missingWarn)
          ? options.missingWarn
          : context.missingWarn;
      const fallbackWarn = isBoolean(options.fallbackWarn)
          ? options.fallbackWarn
          : context.fallbackWarn;
      const part = !!options.part;
      const locale = isString(options.locale) ? options.locale : context.locale;
      const locales = localeFallbacker(context, // eslint-disable-line @typescript-eslint/no-explicit-any
      fallbackLocale, locale);
      if (!isString(key) || key === '') {
          return new Intl.NumberFormat(locale, overrides).format(value);
      }

      // resolve format
      let numberFormat = {};
      let targetLocale;
      let format = null;
      let from = locale;
      let to = null;
      const type = 'number format';
      for (let i = 0; i < locales.length; i++) {
          targetLocale = to = locales[i];
          if (locale !== targetLocale &&
              isTranslateFallbackWarn(fallbackWarn, key)) {
              onWarn(getWarnMessage$1(CoreWarnCodes.FALLBACK_TO_NUMBER_FORMAT, {
                  key,
                  target: targetLocale
              }));
          }
          // for vue-devtools timeline event
          if (locale !== targetLocale) {
              const emitter = context.__v_emitter;
              if (emitter) {
                  emitter.emit("fallback" /* FALBACK */, {
                      type,
                      key,
                      from,
                      to,
                      groupId: `${type}:${key}`
                  });
              }
          }
 
         // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

          // THE LAST LOCALE IN locales ARRAY IS FALLBACK LOCALE 
          // THIS MEANS IF YOU DON'T HAVE LOCALE DECLARED IN numberFormats WITH THE EXPECTED KEY, 
          // YOU WON'T EXIT THE LOOP AND IN THE END  FALLBACK LOCALE WILL BE CHOSEN

         // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

          numberFormat =
              numberFormats[targetLocale] || {};
          format = numberFormat[key];
          if (isPlainObject(format))
              break;
          handleMissing(context, key, targetLocale, missingWarn, type); // eslint-disable-line @typescript-eslint/no-explicit-any
          from = to;
      }
      // checking format and target locale
      if (!isPlainObject(format) || !isString(targetLocale)) {
          return unresolving ? NOT_REOSLVED : key;
      }
      let id = `${targetLocale}__${key}`;
      if (!isEmptyObject(overrides)) {
          id = `${id}__${JSON.stringify(overrides)}`;
      }
      let formatter = __numberFormatters.get(id);
      if (!formatter) {

           // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

          // HERE IN overrides I HAVE AN OBJECT GENERATED FROM Intl.NumberFormat(this.locale, this.format).resolvedOptions(),
          // SO IT'S ABSOLUTELY NOT NECESSARY TO HAVE ANYTHING DECLARED IN numberFormats,
          // ALL THOSE minimumFractionDigits: 0 and  maximumFractionDigits: 2 ARE TAKEN FROM Intl.
          // BUT EVEN WITH THE CORRECT LOCALE-SPECIFIC OBJECT IN new Intl.NumberFormat MAIN IS THE FIRST ARGUMENT - targetLocale
          // BECAUSE NEXT 2 EXAMPLES GENERATE DIFFERENT PARTS

          // new Intl.NumberFormat('en', {"style":"currency","currency":"USD","currencyDisplay":"symbol","currencySign":"standard","minimumIntegerDigits":1,"minimumFractionDigits":0,"maximumFractionDigits":0,"useGrouping":"auto","notation":"standard","signDisplay":"auto","roundingMode":"halfExpand","roundingIncrement":1,"trailingZeroDisplay":"auto","roundingPriority":"auto"}).formatToParts(147000.10)

          // new Intl.NumberFormat('uk', {"style":"currency","currency":"USD","currencyDisplay":"symbol","currencySign":"standard","minimumIntegerDigits":1,"minimumFractionDigits":0,"maximumFractionDigits":0,"useGrouping":"auto","notation":"standard","signDisplay":"auto","roundingMode":"halfExpand","roundingIncrement":1,"trailingZeroDisplay":"auto","roundingPriority":"auto"}).formatToParts(147000.10)
         
          // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

          formatter = new Intl.NumberFormat(targetLocale, assign({}, format, overrides));
          __numberFormatters.set(id, formatter);
      }
      return !part ? formatter.format(value) : formatter.formatToParts(value);
  }

Alternative

Maybe it's possible to configure format options in a different way? I see that inside generated object with Intl.NumberFormat().resolvedOptions() many properties are set to auto

Additional context

No response

Validations

cooperok avatar Jul 19 '23 21:07 cooperok