intlify / vue-i18n

Vue I18n for Vue 3
https://vue-i18n.intlify.dev/
MIT License
2.19k stars 336 forks source link

`part` option for `n` fails with an error #1165

Open mv-go opened 2 years ago

mv-go commented 2 years ago

Reporting a bug?

Using part option for number formatter throws an error. JSfiddle below.

Example usage:

const i18n = createI18n({
  locale: 'en',
  legacy: false,
  numberFormats: {
    en: {
      currency: {
        style: 'currency',
        currencyDisplay: 'narrowSymbol',
      },
    }
})

const { n } = useI18n()

const parts = n(100, { key: 'currency', part: true, currency: 'USD' })

Expected behavior

Reproduction

https://jsfiddle.net/qagtkzxj/39/

System Info

System:
    OS: macOS 12.5.1
    CPU: (8) arm64 Apple M1
    Memory: 80.39 MB / 8.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.16.0 - ~/.nvm/versions/node/v16.16.0/bin/node
    Yarn: 1.22.18 - /opt/homebrew/bin/yarn
    npm: 8.11.0 - ~/.nvm/versions/node/v16.16.0/bin/npm
  Browsers:
    Chrome: 104.0.5112.101
    Firefox: 94.0.2
    Firefox Developer Edition: 105.0
    Safari: 15.6.1
  npmPackages:
    vue: ^3.2.36 => 3.2.37 
    vue-chartjs: ^4.1.1 => 4.1.1 
    vue-i18n: ^9.2.2 => 9.2.2 
    vue-router: ^4.1.5 => 4.1.5

Screenshot

No response

Additional context

No response

Validations

kazupon commented 2 years ago

Thank you for your reporting!

We cannot use the part option with n. api docs said, n returns only string type.

if you would like to use Intl.NumberFormatter.prototype.formatToParts, you need to use the Number format component i18n-n . https://vue-i18n.intlify.dev/guide/essentials/number.html#custom-formatting

I would like to know the use case why you would like to use formatToParts and equivalent in n. Maybe, it’s useful for everyone else as well.

mv-go commented 2 years ago

Hi, @kazupon !

Thanks for replying!

My use case is the following:

We are working with high-precision numbers in my project. Precision sometimes goes up to 18 fractional digits.

So I can have a value = 0.0123456789012345678. JS is not capable of working with precision over 15 digits. So even declaring such value const value = ... and then logging it console.log(value) will log a rounded number - some precision will be lost. Something similar is actually happening when passing such precise numbers from BE to FE (or vice versa) using JSON.

We have decided that BE will provide to the FE such values as strings. And FE would have to display them to users localised. This is where i18n comes into play.

Obviously I can not pass strings to n and passing string directly to Intl.NumberFormat converts them to number first under the hood. So the precision is lost again.

My idea was to do something along the lines of

const customFormatter = (value: string) => {
  const asNumber = Number(value) // so precision is lost, but it's OK
  const asParts = n(asNumber, { key: 'myNumberFormat', part: true })

  const jsonDecimalSeparator = '.' // decimal separator used by my BE for numbers-as-string
  const separatorIndex = value.indexOf(jsonDecimalSeparator) // index of a separator in the original string
  const preciseInteger: string = value.slice(0, separatorIndex) // precise integer part as string
  const preciseFraction: string = value.slice(separatorIndex + jsonDecimalSeparator.length, value.length) // precise fraction part as string

  // here is the "clever" part - replacing parts of formatted number with their respective precise values
  const asPrecisePartsFlattened = asParts.map(part => {
    if (part.type === 'integer') return preciseInteger
    if (part.type === 'fractions') return preciseFraction

    return part.value
  })

  return asPrecisePartsFlattened.join('') // return a formatted string
}

This can obviously be extended to add reactivity, support for whole-integer numbers, different formats, etc.

Actually, while writing this, it got me thinking that folks working with (not-yet) supported currencies - e.g. cryptos of all sorts - could also benefit from this.

const customCurrencies = ['Crypto1, Crypto2']

const customFormat = (value: number, currency: string) => {
  if (!customCurrencies.includes(currency)) return n(value, { key: 'currencyFormat', currency })

  const asParts = n(value, { key: 'currencyFormat', currency: 'USD' }) // using USD to not cause Intl error

  return asParts
    .map(part => {
      if (part.type !== 'currency') return part

      return {
        ...part
        value: currency // replacing USD to our custom currency
      }
    })
    .map(part => part.value) // flatten - collect parts values together
    .join('') // return a formatted string
}
kazupon commented 2 years ago

Thanks for your detail use-case! It helps me understand your use case. I will try to support formatToParts in n.