prantlf / dayjs

Extended fork of Day.js - 2KB immutable date library alternative to Moment.js
https://github.com/prantlf/dayjs
MIT License
35 stars 5 forks source link

Pluralization issue with Russian locale (and possibly others) #4

Open leovp opened 6 years ago

leovp commented 6 years ago

New locale handling is pretty cool, thanks. But I think I found a bug in relativeTime plugin.

So, there are three declensions of nouns in Russian: singular, dual and plural. The library does have h, hh and hhh for all of those (this is the case with other units as well), but it assumes that single-letter format is only used when the value of whatever unit is exactly 1. For example "час назад" ("an hour ago"). Then if the last digit of value is less than 5, hh handles it, otherwise it's going to be hhh.

In Russian though (and I assume a few other languages), singular rule works for 1, 21, 31, 41, ..., 101, etc. Then dual works for 2, 3, 4, 22, 23, 24, ..., 102, 103, 104, 122, 123, 124, etc. Then plural works for everything else. See Reference for a complete formula.

Example of a bug:

dayjs('2018-10-04 15:00:00').to('2018-10-04 15:21:00')
"через 21 минут" 

It should be "через 21 минуту", same as if it was 1 minute ("через минуту"). I think single-letter formats should have a "%d" in there as well, so that they work for all units that are singular.

Relevant code comment: https://github.com/prantlf/dayjs/blob/combined/src/plugin/relativeTime/index.js#L103-L106 Reference: http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html

prantlf commented 6 years ago

Thanks a lot, @leovp! Especially the second link and the links further on from it are great source of information.

My original simplification was quick to implement, but not enough. Even the common languages were not covered well with the three plural forms and the single rule for them. I went for the universal solution as done by Mozilla localizers. Newly I specify both the plural rule (a number or a function) and the plural forms as an array of strings. The plural rule function returns an index to the array of plural forms.

  relativeTime: {
    // 3 plural forms for 1 and x1, 2-4 and x2-4, 5-
    pluralRule: 7,
    duration: {
      s: 'несколько секунд',
      m: 'минута',
      mm: ['%d минута', '%d минуты', '%d минут'],
      h: 'час',
      hh: ['%d час', '%d часа', '%d часов'],
      d: 'день',
      dd: ['%d день', '%d дня', '%d дней'],
      M: 'месяц',
      MM: ['%d месяц', '%d месяца', '%d месяцев'],
      y: 'год',
      yy: ['%d год', '%d года', '%d лет']
    },
    future: {
      s: 'через несколько секунд',
      m: 'через минуту',
      mm: ['через %d минуту', 'через %d минуты', 'через %d минут'],
      h: 'через час',
      hh: ['через %d час', 'через %d часа', 'через %d часов'],
      d: 'завтра',
      dd: ['через %d день', 'через %d дня', 'через %d дней'],
      M: 'через месяц',
      MM: ['через %d месяц', 'через %d месяца', 'через %d месяцев'],
      y: 'через год',
      yy: ['через %d год', 'через %d года', 'через %d лет']
    },
    past: {
      s: 'несколько секунд назад',
      m: 'минуту назад',
      mm: ['%d минуту назад', '%d минуты назад', '%d минут назад'],
      h: 'час назад',
      hh: ['%d час назад', '%d часа назад', '%d часов назад'],
      d: 'вчера',
      dd: ['%d день назад', '%d дня назад', '%d дней назад'],
      M: 'месяц назад',
      MM: ['%d месяц назад', '%d месяца назад', '%d месяцев назад'],
      y: 'в прошлом году',
      yy: ['%d год назад', '%d года назад', '%d лет назад']
    }
  }
  // Plural rule #7 (3 forms)
  // Families: Slavic (Belarusian, Bosnian, Croatian, Serbian, Russian, Ukrainian)
  n => n % 10 === 1 && n % 100 !== 11 ? 0
    : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2,

I had to to separate the special singular form from the plural forms. It has usually no number and thus the first plural form (for 1, 21, 31, ...) cannot be reused for it.

I released this feature in 1.12.2. At first, locales cs, ru, sk and ua are correct there.

Eventually, the plural rules should make it out of the relativeTime plugin to the dayjs core utilities. Or may be event out of this module.