Skip to content

Commit 1bb9c6b

Browse files
committed
fix: Change the locale structure to support a special singular form
Number 1 is not formatted using the plural forms as usual. There is an additional text added for this case.
1 parent f39fb48 commit 1bb9c6b

File tree

6 files changed

+171
-106
lines changed

6 files changed

+171
-106
lines changed

docs/en/I18n.md

+38-21
Original file line numberDiff line numberDiff line change
@@ -108,39 +108,56 @@ Old template of the part of a Day.js locale Object for the RelativeTime plugin.
108108
}
109109
```
110110

111-
New template of the part of a Day.js locale Object for the RelativeTime plugin. It works well for fusional languages which decline nouns and which habe two form of plural. Slavic languages like Czech language, for example. The `duration` expressions will be used if the `withoutSuffix` parameter is set to `true` in the method calls. The plural rule is an index of a function in the array in `src/plugins/relativeTime/pluralRules.js`, of a function itself. The function gets a number and return an index of the plural form to use. The keys with time units point to arrays with plural forms.
111+
New template of the part of a Day.js locale Object for the RelativeTime plugin. It works well for fusional languages which decline nouns and which have two forms of plural. Slavic languages like Czech language, for example. The `duration` expressions will be used if the `withoutSuffix` parameter is set to `true` in the method calls.
112112
```javascript
113113
relativeTime: { // relative time format strings, keep %d as the same
114114
// Plural rule for 3 plural forms in Slavic languages
115115
pluralRule: 8,
116116
duration: {
117-
// Static message, just one plural form needed
118-
s: ['několik sekund'],
119-
// for 1 minute, for 2-4 minutes, for 5 and more minutes
120-
m: ['minuta', '%d minuty', '%d minut'],
121-
h: ['hodina', '%d hodiny', '%d hodin'],
122-
d: ['den', '%d dny', '%d dní'],
123-
M: ['měsíc', '%d měsíce', '%d měsícú'],
124-
y: ['rok', '%d roky', '%d let']
117+
// Static message, just one singular/plural form needed
118+
s: 'několik sekund',
119+
// Static message for a single minute without any number
120+
m: 'minuta',
121+
// Plural forms for 1, 2 to 4, and 5 and more minutes
122+
mm: ['%d minuta', '%d minuty', '%d minut'],
123+
h: 'hodina',
124+
hh: ['%d hodina', '%d hodiny', '%d hodin'],
125+
d: 'den',
126+
dd: ['%d den', '%d dny', '%d dní'],
127+
M: 'měsíc',
128+
MM: ['%d měsíc', '%d měsíce', '%d měsícú'],
129+
y: 'rok',
130+
yy: ['%d rok', '%d roky', '%d let']
125131
},
126132
future: {
127-
s: ['za několik sekund'],
128-
m: ['za minutu', 'za %d minuty', 'za %d minut'],
129-
h: ['za hodinu', 'za %d hodiny', 'za %d hodin'],
130-
d: ['zítra', 'za %d dny', 'za %d dní'],
131-
M: ['za měsíc', 'za %d měsíce', 'za %d měsícú'],
132-
y: ['za rok', 'za %d roky', 'za %d let']
133+
s: 'za několik sekund',
134+
m: 'za minutu',
135+
mm: ['za %d minutu', 'za %d minuty', 'za %d minut'],
136+
h: 'za hodinu',
137+
hh: ['za %d hodinu', 'za %d hodiny', 'za %d hodin'],
138+
d: 'zítra',
139+
dd: ['za %d den', 'za %d dny', 'za %d dní'],
140+
M: 'za měsíc',
141+
MM: ['za %d měsíc', 'za %d měsíce', 'za %d měsícú'],
142+
y: 'za rok',
143+
yy: ['za %d rok', 'za %d roky', 'za %d let']
133144
},
134145
past: {
135-
s: ['před několika sekundami'],
136-
m: ['před minutou', 'před %d minutami', 'před %d minutami'],
137-
h: ['před hodinou', 'před %d hodinami', 'před %d hodinami'],
138-
d: ['včera', 'před %d dny', 'před %d dny'],
139-
M: ['před měsícem', 'před %d měsíci', 'před %d měsíci'],
140-
y: ['vloni', 'před %d roky', 'před %d lety']
146+
s: 'před několika sekundami',
147+
m: 'před minutou',
148+
mm: ['před %d minutou', 'před %d minutami', 'před %d minutami'],
149+
h: 'před hodinou',
150+
hh: ['před %d hodinou', 'před %d hodinami', 'před %d hodinami'],
151+
d: 'včera',
152+
dd: ['před %d dnem', 'před %d dny', 'před %d dny'],
153+
M: 'před měsícem',
154+
MM: ['před %d měsícem', 'před %d měsíci', 'před %d měsíci'],
155+
y: 'vloni',
156+
yy: ['před %d rokem', 'před %d roky', 'před %d lety']
141157
}
142158
}
143159
```
160+
The plural rule is an index of a function in the array in `src/plugins/relativeTime/pluralRules.js`, or an actual function. The function gets a number and return an index of the plural form to use. The keys with two-letter time units point to arrays with plural forms. The keys with single-letter time units point to a string shown for a single value, usually without any number.
144161

145162
Template of a Day.js locale file.
146163
```javascript

src/locale/cs.js

+33-18
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,43 @@ const locale = {
99
// 3 plural forms for 1, 2-4, 5-
1010
pluralRule: 8,
1111
duration: {
12-
s: ['několik sekund'],
13-
m: ['minuta', '%d minuty', '%d minut'],
14-
h: ['hodina', '%d hodiny', '%d hodin'],
15-
d: ['den', '%d dny', '%d dní'],
16-
M: ['měsíc', '%d měsíce', '%d měsícú'],
17-
y: ['rok', '%d roky', '%d let']
12+
s: 'několik sekund',
13+
m: 'minuta',
14+
mm: ['%d minuta', '%d minuty', '%d minut'],
15+
h: 'hodina',
16+
hh: ['%d hodina', '%d hodiny', '%d hodin'],
17+
d: 'den',
18+
dd: ['%d den', '%d dny', '%d dní'],
19+
M: 'měsíc',
20+
MM: ['%d měsíc', '%d měsíce', '%d měsícú'],
21+
y: 'rok',
22+
yy: ['%d rok', '%d roky', '%d let']
1823
},
1924
future: {
20-
s: ['za několik sekund'],
21-
m: ['za minutu', 'za %d minuty', 'za %d minut'],
22-
h: ['za hodinu', 'za %d hodiny', 'za %d hodin'],
23-
d: ['zítra', 'za %d dny', 'za %d dní'],
24-
M: ['za měsíc', 'za %d měsíce', 'za %d měsícú'],
25-
y: ['za rok', 'za %d roky', 'za %d let']
25+
s: 'za několik sekund',
26+
m: 'za minutu',
27+
mm: ['za %d minutu', 'za %d minuty', 'za %d minut'],
28+
h: 'za hodinu',
29+
hh: ['za %d hodinu', 'za %d hodiny', 'za %d hodin'],
30+
d: 'zítra',
31+
dd: ['za %d den', 'za %d dny', 'za %d dní'],
32+
M: 'za měsíc',
33+
MM: ['za %d měsíc', 'za %d měsíce', 'za %d měsícú'],
34+
y: 'za rok',
35+
yy: ['za %d rok', 'za %d roky', 'za %d let']
2636
},
2737
past: {
28-
s: ['před několika sekundami'],
29-
m: ['před minutou', 'před %d minutami', 'před %d minutami'],
30-
h: ['před hodinou', 'před %d hodinami', 'před %d hodinami'],
31-
d: ['včera', 'před %d dny', 'před %d dny'],
32-
M: ['před měsícem', 'před %d měsíci', 'před %d měsíci'],
33-
y: ['vloni', 'před %d roky', 'před %d lety']
38+
s: 'před několika sekundami',
39+
m: 'před minutou',
40+
mm: ['před %d minutou', 'před %d minutami', 'před %d minutami'],
41+
h: 'před hodinou',
42+
hh: ['před %d hodinou', 'před %d hodinami', 'před %d hodinami'],
43+
d: 'včera',
44+
dd: ['před %d dnem', 'před %d dny', 'před %d dny'],
45+
M: 'před měsícem',
46+
MM: ['před %d měsícem', 'před %d měsíci', 'před %d měsíci'],
47+
y: 'vloni',
48+
yy: ['před %d rokem', 'před %d roky', 'před %d lety']
3449
}
3550
}
3651
}

src/locale/ru.js

+33-18
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,43 @@ const locale = {
88
// 3 plural forms for 1 and x1, 2-4 and x2-4, 5-
99
pluralRule: 7,
1010
duration: {
11-
s: ['несколько секунд'],
12-
m: ['минута', '%d минуты', '%d минут'],
13-
h: ['час', '%d часа', '%d часов'],
14-
d: ['день', '%d дни', '%d дней'],
15-
M: ['месяц', '%d месяца', '%d месяцев'],
16-
y: ['год', '%d годы', '%d лет']
11+
s: 'несколько секунд',
12+
m: 'минута',
13+
mm: ['%d минута', '%d минуты', '%d минут'],
14+
h: 'час',
15+
hh: ['%d час', '%d часа', '%d часов'],
16+
d: 'день',
17+
dd: ['%d день', '%d дни', '%d дней'],
18+
M: 'месяц',
19+
MM: ['%d месяц', '%d месяца', '%d месяцев'],
20+
y: 'год',
21+
yy: ['%d год', '%d годы', '%d лет']
1722
},
1823
future: {
19-
s: ['через несколько секунд'],
20-
m: ['через минуту', 'через %d минуты', 'через %d минут'],
21-
h: ['через час', 'через %d часа', 'через %d часов'],
22-
d: ['завтра', 'через %d дни', 'через %d дней'],
23-
M: ['через месяц', 'через %d месяца', 'через %d месяцев'],
24-
y: ['через год', 'через %d годы', 'через %d лет']
24+
s: 'через несколько секунд',
25+
m: 'через минуту',
26+
mm: ['через %d минуту', 'через %d минуты', 'через %d минут'],
27+
h: 'через час',
28+
hh: ['через %d час', 'через %d часа', 'через %d часов'],
29+
d: 'завтра',
30+
dd: ['через %d день', 'через %d дни', 'через %d дней'],
31+
M: 'через месяц',
32+
MM: ['через %d месяц', 'через %d месяца', 'через %d месяцев'],
33+
y: 'через год',
34+
yy: ['через %d год', 'через %d годы', 'через %d лет']
2535
},
2636
past: {
27-
s: ['несколько секунд назад'],
28-
m: ['минуту назад', '%d минуты назад', '%d минут назад'],
29-
h: ['час назад', '%d часа назад', '%d часов назад'],
30-
d: ['вчера', '%d дни назад', '%d дней назад'],
31-
M: ['месяц назад', '%d месяца назад', '%d месяцев назад'],
32-
y: ['в прошлом году', '%d годы назад', '%d лет назад']
37+
s: 'несколько секунд назад',
38+
m: 'минуту назад',
39+
mm: ['%d минуту назад', '%d минуты назад', '%d минут назад'],
40+
h: 'час назад',
41+
hh: ['%d час назад', '%d часа назад', '%d часов назад'],
42+
d: 'вчера',
43+
dd: ['%d день назад', '%d дни назад', '%d дней назад'],
44+
M: 'месяц назад',
45+
MM: ['%d месяц назад', '%d месяца назад', '%d месяцев назад'],
46+
y: 'в прошлом году',
47+
yy: ['%d год назад', '%d годы назад', '%d лет назад']
3348
}
3449
},
3550
ordinal: n => n

src/plugin/relativeTime/index.js

+49-39
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as C from '../../constant'
22
import pluralRules from './pluralRules'
33

4-
// Returns 0 for singular and 1 for plural for languages with a single plural
5-
const simplePluralRule = 1
6-
// Returns 0 for singular, 1 for plural for 2 <= value <= 4 and 2 for plural
4+
// Special plural rules for upgraded locales, which are not complete
5+
// Returns 0 for plural for languages with a single plural
6+
const simplePluralRule = () => 0
7+
// Returns 0 for plural for 2 <= value <= 4 and 1 for plural
78
// for value >= 5, which is sufficient for some languages like Czech
8-
const improvedPluralRule = 8
9+
const improvedPluralRule = n => n >= 2 && n <= 4 ? 0 : 1 // eslint-disable-line no-confusing-arrow
910

1011
export default (o, c, d) => {
1112
const proto = c.prototype
@@ -37,12 +38,10 @@ export default (o, c, d) => {
3738
const kl = key.length
3839
// Skip entries in the locale, which do not format numerals (future and past)
3940
if (kl <= 2) {
40-
// Array of plurals uses just one-letter keys
41-
const unit = key[0]
42-
// Make sure, that the unit-formatting string has an array of plurals
43-
const pluralForms = result[unit] || (result[unit] = [])
44-
// Make sure, that singular comes before the plural in the array
45-
pluralForms[kl - 1] = loc[key]
41+
// Save the special singular without any number with the single-letter key and the
42+
// single plural to be used with any number greater then 1 with the two-letter key
43+
const text = loc[key]
44+
result[key] = kl === 1 ? text : [text]
4645
}
4746
// Remove the original locale entry; the original locale object needs
4847
// to be retained to prevent upgrading on every formatting call
@@ -53,15 +52,22 @@ export default (o, c, d) => {
5352
const futures = {}
5453
const pasts = {}
5554
Object.keys(durations).forEach((key) => {
56-
const pluralForms = durations[key]
57-
futures[key] = pluralForms.map(pluralForm => future.replace('%s', pluralForm))
58-
pasts[key] = pluralForms.map(pluralForm => past.replace('%s', pluralForm))
55+
const value = durations[key]
56+
if (typeof value === 'string') {
57+
// Handle singular texts
58+
futures[key] = future.replace('%s', value)
59+
pasts[key] = past.replace('%s', value)
60+
} else {
61+
// Handle plural texts
62+
futures[key] = value.map(pluralForm => future.replace('%s', pluralForm))
63+
pasts[key] = value.map(pluralForm => past.replace('%s', pluralForm))
64+
}
5965
})
6066
// Set localized expressions for durations, future and past to the locale
6167
loc.duration = durations
6268
loc.future = futures
6369
loc.past = pasts
64-
// Recognize a singular and only one plural like in English
70+
// Recognize only one plural like in English
6571
loc.pluralRule = simplePluralRule
6672
}
6773
// Upgrade the improved, but not the final version of the localization,
@@ -75,34 +81,40 @@ export default (o, c, d) => {
7581
function convertPlurals(object) {
7682
return Object.keys(object).reduce((result, key) => {
7783
const kl = key.length
78-
// Array of plurals uses just one-letter keys
79-
const unit = key[0]
80-
// Make sure, that the unit-formatting string has an array of plurals
81-
const pluralForms = result[unit] || (result[unit] = [])
82-
// Make sure, that singular comes before plurals and plurals come in
83-
// the right order
84-
pluralForms[kl - 1] = object[key]
84+
const text = object[key]
85+
if (kl === 1) {
86+
// Leave the special singular without any number as-is
87+
result[key] = text
88+
} else {
89+
// Array of plurals uses the two-letter key
90+
const singularUnit = key[0]
91+
const pluralUnit = singularUnit + singularUnit
92+
// Make sure, that the unit-formatting string contains an array
93+
const pluralForms = result[pluralUnit] || (result[pluralUnit] = [])
94+
// Make sure, that the plural for 2-4 comes before the others in the array
95+
pluralForms[kl - 2] = text
96+
}
8597
return result
8698
}, {})
8799
}
88100
// Set localized expressions for durations, future and past to the locale
89101
loc.duration = convertPlurals(loc.duration)
90102
loc.future = convertPlurals(loc.future)
91103
loc.past = convertPlurals(loc.past)
92-
// Recognize a singular and two plurals like in Czech
104+
// Recognize two plurals like in the Czech language
93105
loc.pluralRule = improvedPluralRule
94106
}
95107
// Upgrades old locale format to provide compatibility with older
96108
// localizations; the grammar may not be correct for fusional languages
97109
// {
98-
// duration: { s: ['...'], m: ['...', '...', ...] },
110+
// duration: { s: '...', m: '...', mm: ['...', '...', ...] },
99111
// future: { ... }, past: { ... }, pluralRule: N
100112
// }
101113
function upgradeLocale(loc) {
102114
// Do not upgrade already upgraded locales
103115
if (loc.s) {
104116
upgradeSimpleLocale(loc)
105-
} else if (typeof loc.duration.s === 'string') {
117+
} else if (typeof loc.duration.mm === 'string') {
106118
upgradeImprovedLocale(loc)
107119
}
108120
}
@@ -111,15 +123,15 @@ export default (o, c, d) => {
111123
const T = [
112124
{ l: 's', r: 44, d: C.S },
113125
{ l: 'm', r: 89 },
114-
{ l: 'm', r: 44, d: C.MIN, m: true }, // eslint-disable-line object-curly-newline
126+
{ l: 'mm', r: 44, d: C.MIN },
115127
{ l: 'h', r: 89 },
116-
{ l: 'h', r: 21, d: C.H, m: true }, // eslint-disable-line object-curly-newline
128+
{ l: 'hh', r: 21, d: C.H },
117129
{ l: 'd', r: 35 },
118-
{ l: 'd', r: 25, d: C.D, m: true }, // eslint-disable-line object-curly-newline
130+
{ l: 'dd', r: 25, d: C.D },
119131
{ l: 'M', r: 45 },
120-
{ l: 'M', r: 10, d: C.M, m: true }, // eslint-disable-line object-curly-newline
132+
{ l: 'MM', r: 10, d: C.M },
121133
{ l: 'y', r: 17 },
122-
{ l: 'y', d: C.Y, m: true }
134+
{ l: 'yy', d: C.Y }
123135
]
124136
const Tl = T.length
125137
let result
@@ -149,21 +161,19 @@ export default (o, c, d) => {
149161
loc = locs.past
150162
}
151163
const key = t.l
152-
const pluralForms = loc[key]
153-
let pluralFormIndex
154-
if (t.m) {
155-
// Compute the index in the array of localized expressions;
156-
// zero is singular, then come plurals
164+
if (key.length === 1) {
165+
// Handle singular using a special text without any number
166+
out = loc[key]
167+
} else {
168+
// Choose the plural form using the index decided by the plural rule
157169
let { pluralRule } = locs
158170
if (typeof pluralRule === 'number') {
159171
pluralRule = pluralRules[pluralRule]
160172
}
161-
pluralFormIndex = pluralRule(abs)
162-
} else {
163-
// Singular is always the first item in the array
164-
pluralFormIndex = 0
173+
const pluralForms = loc[key]
174+
const pluralFormIndex = pluralRule(abs)
175+
out = pluralForms[pluralFormIndex].replace('%d', abs)
165176
}
166-
out = pluralForms[pluralFormIndex].replace('%d', abs)
167177
break
168178
}
169179
}

test/locale/keys.test.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ it('Locale keys', () => {
4848
// Improved locale object structure
4949
expect(Object.keys(relativeTime).sort()).toEqual(['duration', 'future', 'past'].sort());
5050
['duration', 'future', 'past'].forEach(key =>
51+
// eslint-disable-next-line implicit-arrow-linebreak
5152
expect(Object.keys(relativeTime[key]).sort())
5253
.toEqual(['d', 'dd', 'ddd', 'h', 'hh', 'hhh', 'm', 'mm', 'mmm',
5354
'M', 'MM', 'MMM', 's', 'y', 'yy', 'yyy'].sort()));
5455
['duration', 'future', 'past'].forEach(key =>
56+
// eslint-disable-next-line implicit-arrow-linebreak
5557
Object.keys(relativeTime[key]).forEach(key2 =>
5658
// eslint-disable-next-line implicit-arrow-linebreak
5759
expect(typeof relativeTime[key][key2]).toEqual('string')))
@@ -61,12 +63,16 @@ it('Locale keys', () => {
6163
['duration', 'future', 'past'].forEach(key =>
6264
// eslint-disable-next-line implicit-arrow-linebreak
6365
expect(Object.keys(relativeTime[key]).sort())
64-
.toEqual(['d', 'h', 'm', 'M', 's', 'y'].sort()));
66+
.toEqual(['M', 'MM', 'd', 'dd', 'h', 'hh', 'm', 'mm', 's', 'y', 'yy'].sort()));
6567
['duration', 'future', 'past'].forEach(key =>
6668
// eslint-disable-next-line implicit-arrow-linebreak
67-
expect(Object.keys(relativeTime[key]).every(key2 =>
68-
// eslint-disable-next-line implicit-arrow-linebreak
69-
Array.isArray(relativeTime[key][key2]))).toBeTruthy())
69+
Object.keys(relativeTime[key]).forEach((key2) => {
70+
if (key2.length === 1) {
71+
expect(typeof relativeTime[key][key2]).toEqual('string')
72+
} else {
73+
expect(Array.isArray(relativeTime[key][key2])).toBeTruthy()
74+
}
75+
}))
7076
expect(typeof relativeTime.pluralRule === 'number'
7177
|| typeof relativeTime.pluralRule === 'function').toBeTruthy()
7278
}

0 commit comments

Comments
 (0)