Skip to content

Commit

Permalink
fix: Change the locale structure to support a special singular form
Browse files Browse the repository at this point in the history
Number 1 is not formatted using the plural forms as usual. There is an additional text added for this case.
  • Loading branch information
prantlf committed Oct 14, 2018
1 parent f39fb48 commit 1bb9c6b
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 106 deletions.
59 changes: 38 additions & 21 deletions docs/en/I18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,39 +108,56 @@ Old template of the part of a Day.js locale Object for the RelativeTime plugin.
}
```

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.
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.
```javascript
relativeTime: { // relative time format strings, keep %d as the same
// Plural rule for 3 plural forms in Slavic languages
pluralRule: 8,
duration: {
// Static message, just one plural form needed
s: ['několik sekund'],
// for 1 minute, for 2-4 minutes, for 5 and more minutes
m: ['minuta', '%d minuty', '%d minut'],
h: ['hodina', '%d hodiny', '%d hodin'],
d: ['den', '%d dny', '%d dní'],
M: ['měsíc', '%d měsíce', '%d měsícú'],
y: ['rok', '%d roky', '%d let']
// Static message, just one singular/plural form needed
s: 'několik sekund',
// Static message for a single minute without any number
m: 'minuta',
// Plural forms for 1, 2 to 4, and 5 and more minutes
mm: ['%d minuta', '%d minuty', '%d minut'],
h: 'hodina',
hh: ['%d hodina', '%d hodiny', '%d hodin'],
d: 'den',
dd: ['%d den', '%d dny', '%d dní'],
M: 'měsíc',
MM: ['%d měsíc', '%d měsíce', '%d měsícú'],
y: 'rok',
yy: ['%d rok', '%d roky', '%d let']
},
future: {
s: ['za několik sekund'],
m: ['za minutu', 'za %d minuty', 'za %d minut'],
h: ['za hodinu', 'za %d hodiny', 'za %d hodin'],
d: ['zítra', 'za %d dny', 'za %d dní'],
M: ['za měsíc', 'za %d měsíce', 'za %d měsícú'],
y: ['za rok', 'za %d roky', 'za %d let']
s: 'za několik sekund',
m: 'za minutu',
mm: ['za %d minutu', 'za %d minuty', 'za %d minut'],
h: 'za hodinu',
hh: ['za %d hodinu', 'za %d hodiny', 'za %d hodin'],
d: 'zítra',
dd: ['za %d den', 'za %d dny', 'za %d dní'],
M: 'za měsíc',
MM: ['za %d měsíc', 'za %d měsíce', 'za %d měsícú'],
y: 'za rok',
yy: ['za %d rok', 'za %d roky', 'za %d let']
},
past: {
s: ['před několika sekundami'],
m: ['před minutou', 'před %d minutami', 'před %d minutami'],
h: ['před hodinou', 'před %d hodinami', 'před %d hodinami'],
d: ['včera', 'před %d dny', 'před %d dny'],
M: ['před měsícem', 'před %d měsíci', 'před %d měsíci'],
y: ['vloni', 'před %d roky', 'před %d lety']
s: 'před několika sekundami',
m: 'před minutou',
mm: ['před %d minutou', 'před %d minutami', 'před %d minutami'],
h: 'před hodinou',
hh: ['před %d hodinou', 'před %d hodinami', 'před %d hodinami'],
d: 'včera',
dd: ['před %d dnem', 'před %d dny', 'před %d dny'],
M: 'před měsícem',
MM: ['před %d měsícem', 'před %d měsíci', 'před %d měsíci'],
y: 'vloni',
yy: ['před %d rokem', 'před %d roky', 'před %d lety']
}
}
```
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.

Template of a Day.js locale file.
```javascript
Expand Down
51 changes: 33 additions & 18 deletions src/locale/cs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,43 @@ const locale = {
// 3 plural forms for 1, 2-4, 5-
pluralRule: 8,
duration: {
s: ['několik sekund'],
m: ['minuta', '%d minuty', '%d minut'],
h: ['hodina', '%d hodiny', '%d hodin'],
d: ['den', '%d dny', '%d dní'],
M: ['měsíc', '%d měsíce', '%d měsícú'],
y: ['rok', '%d roky', '%d let']
s: 'několik sekund',
m: 'minuta',
mm: ['%d minuta', '%d minuty', '%d minut'],
h: 'hodina',
hh: ['%d hodina', '%d hodiny', '%d hodin'],
d: 'den',
dd: ['%d den', '%d dny', '%d dní'],
M: 'měsíc',
MM: ['%d měsíc', '%d měsíce', '%d měsícú'],
y: 'rok',
yy: ['%d rok', '%d roky', '%d let']
},
future: {
s: ['za několik sekund'],
m: ['za minutu', 'za %d minuty', 'za %d minut'],
h: ['za hodinu', 'za %d hodiny', 'za %d hodin'],
d: ['zítra', 'za %d dny', 'za %d dní'],
M: ['za měsíc', 'za %d měsíce', 'za %d měsícú'],
y: ['za rok', 'za %d roky', 'za %d let']
s: 'za několik sekund',
m: 'za minutu',
mm: ['za %d minutu', 'za %d minuty', 'za %d minut'],
h: 'za hodinu',
hh: ['za %d hodinu', 'za %d hodiny', 'za %d hodin'],
d: 'zítra',
dd: ['za %d den', 'za %d dny', 'za %d dní'],
M: 'za měsíc',
MM: ['za %d měsíc', 'za %d měsíce', 'za %d měsícú'],
y: 'za rok',
yy: ['za %d rok', 'za %d roky', 'za %d let']
},
past: {
s: ['před několika sekundami'],
m: ['před minutou', 'před %d minutami', 'před %d minutami'],
h: ['před hodinou', 'před %d hodinami', 'před %d hodinami'],
d: ['včera', 'před %d dny', 'před %d dny'],
M: ['před měsícem', 'před %d měsíci', 'před %d měsíci'],
y: ['vloni', 'před %d roky', 'před %d lety']
s: 'před několika sekundami',
m: 'před minutou',
mm: ['před %d minutou', 'před %d minutami', 'před %d minutami'],
h: 'před hodinou',
hh: ['před %d hodinou', 'před %d hodinami', 'před %d hodinami'],
d: 'včera',
dd: ['před %d dnem', 'před %d dny', 'před %d dny'],
M: 'před měsícem',
MM: ['před %d měsícem', 'před %d měsíci', 'před %d měsíci'],
y: 'vloni',
yy: ['před %d rokem', 'před %d roky', 'před %d lety']
}
}
}
Expand Down
51 changes: 33 additions & 18 deletions src/locale/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,43 @@ const locale = {
// 3 plural forms for 1 and x1, 2-4 and x2-4, 5-
pluralRule: 7,
duration: {
s: ['несколько секунд'],
m: ['минута', '%d минуты', '%d минут'],
h: ['час', '%d часа', '%d часов'],
d: ['день', '%d дни', '%d дней'],
M: ['месяц', '%d месяца', '%d месяцев'],
y: ['год', '%d годы', '%d лет']
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: ['через минуту', 'через %d минуты', 'через %d минут'],
h: ['через час', 'через %d часа', 'через %d часов'],
d: ['завтра', 'через %d дни', 'через %d дней'],
M: ['через месяц', 'через %d месяца', 'через %d месяцев'],
y: ['через год', 'через %d годы', 'через %d лет']
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: ['минуту назад', '%d минуты назад', '%d минут назад'],
h: ['час назад', '%d часа назад', '%d часов назад'],
d: ['вчера', '%d дни назад', '%d дней назад'],
M: ['месяц назад', '%d месяца назад', '%d месяцев назад'],
y: ['в прошлом году', '%d годы назад', '%d лет назад']
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 лет назад']
}
},
ordinal: n => n
Expand Down
88 changes: 49 additions & 39 deletions src/plugin/relativeTime/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as C from '../../constant'
import pluralRules from './pluralRules'

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

export default (o, c, d) => {
const proto = c.prototype
Expand Down Expand Up @@ -37,12 +38,10 @@ export default (o, c, d) => {
const kl = key.length
// Skip entries in the locale, which do not format numerals (future and past)
if (kl <= 2) {
// Array of plurals uses just one-letter keys
const unit = key[0]
// Make sure, that the unit-formatting string has an array of plurals
const pluralForms = result[unit] || (result[unit] = [])
// Make sure, that singular comes before the plural in the array
pluralForms[kl - 1] = loc[key]
// Save the special singular without any number with the single-letter key and the
// single plural to be used with any number greater then 1 with the two-letter key
const text = loc[key]
result[key] = kl === 1 ? text : [text]
}
// Remove the original locale entry; the original locale object needs
// to be retained to prevent upgrading on every formatting call
Expand All @@ -53,15 +52,22 @@ export default (o, c, d) => {
const futures = {}
const pasts = {}
Object.keys(durations).forEach((key) => {
const pluralForms = durations[key]
futures[key] = pluralForms.map(pluralForm => future.replace('%s', pluralForm))
pasts[key] = pluralForms.map(pluralForm => past.replace('%s', pluralForm))
const value = durations[key]
if (typeof value === 'string') {
// Handle singular texts
futures[key] = future.replace('%s', value)
pasts[key] = past.replace('%s', value)
} else {
// Handle plural texts
futures[key] = value.map(pluralForm => future.replace('%s', pluralForm))
pasts[key] = value.map(pluralForm => past.replace('%s', pluralForm))
}
})
// Set localized expressions for durations, future and past to the locale
loc.duration = durations
loc.future = futures
loc.past = pasts
// Recognize a singular and only one plural like in English
// Recognize only one plural like in English
loc.pluralRule = simplePluralRule
}
// Upgrade the improved, but not the final version of the localization,
Expand All @@ -75,34 +81,40 @@ export default (o, c, d) => {
function convertPlurals(object) {
return Object.keys(object).reduce((result, key) => {
const kl = key.length
// Array of plurals uses just one-letter keys
const unit = key[0]
// Make sure, that the unit-formatting string has an array of plurals
const pluralForms = result[unit] || (result[unit] = [])
// Make sure, that singular comes before plurals and plurals come in
// the right order
pluralForms[kl - 1] = object[key]
const text = object[key]
if (kl === 1) {
// Leave the special singular without any number as-is
result[key] = text
} else {
// Array of plurals uses the two-letter key
const singularUnit = key[0]
const pluralUnit = singularUnit + singularUnit
// Make sure, that the unit-formatting string contains an array
const pluralForms = result[pluralUnit] || (result[pluralUnit] = [])
// Make sure, that the plural for 2-4 comes before the others in the array
pluralForms[kl - 2] = text
}
return result
}, {})
}
// Set localized expressions for durations, future and past to the locale
loc.duration = convertPlurals(loc.duration)
loc.future = convertPlurals(loc.future)
loc.past = convertPlurals(loc.past)
// Recognize a singular and two plurals like in Czech
// Recognize two plurals like in the Czech language
loc.pluralRule = improvedPluralRule
}
// Upgrades old locale format to provide compatibility with older
// localizations; the grammar may not be correct for fusional languages
// {
// duration: { s: ['...'], m: ['...', '...', ...] },
// duration: { s: '...', m: '...', mm: ['...', '...', ...] },
// future: { ... }, past: { ... }, pluralRule: N
// }
function upgradeLocale(loc) {
// Do not upgrade already upgraded locales
if (loc.s) {
upgradeSimpleLocale(loc)
} else if (typeof loc.duration.s === 'string') {
} else if (typeof loc.duration.mm === 'string') {
upgradeImprovedLocale(loc)
}
}
Expand All @@ -111,15 +123,15 @@ export default (o, c, d) => {
const T = [
{ l: 's', r: 44, d: C.S },
{ l: 'm', r: 89 },
{ l: 'm', r: 44, d: C.MIN, m: true }, // eslint-disable-line object-curly-newline
{ l: 'mm', r: 44, d: C.MIN },
{ l: 'h', r: 89 },
{ l: 'h', r: 21, d: C.H, m: true }, // eslint-disable-line object-curly-newline
{ l: 'hh', r: 21, d: C.H },
{ l: 'd', r: 35 },
{ l: 'd', r: 25, d: C.D, m: true }, // eslint-disable-line object-curly-newline
{ l: 'dd', r: 25, d: C.D },
{ l: 'M', r: 45 },
{ l: 'M', r: 10, d: C.M, m: true }, // eslint-disable-line object-curly-newline
{ l: 'MM', r: 10, d: C.M },
{ l: 'y', r: 17 },
{ l: 'y', d: C.Y, m: true }
{ l: 'yy', d: C.Y }
]
const Tl = T.length
let result
Expand Down Expand Up @@ -149,21 +161,19 @@ export default (o, c, d) => {
loc = locs.past
}
const key = t.l
const pluralForms = loc[key]
let pluralFormIndex
if (t.m) {
// Compute the index in the array of localized expressions;
// zero is singular, then come plurals
if (key.length === 1) {
// Handle singular using a special text without any number
out = loc[key]
} else {
// Choose the plural form using the index decided by the plural rule
let { pluralRule } = locs
if (typeof pluralRule === 'number') {
pluralRule = pluralRules[pluralRule]
}
pluralFormIndex = pluralRule(abs)
} else {
// Singular is always the first item in the array
pluralFormIndex = 0
const pluralForms = loc[key]
const pluralFormIndex = pluralRule(abs)
out = pluralForms[pluralFormIndex].replace('%d', abs)
}
out = pluralForms[pluralFormIndex].replace('%d', abs)
break
}
}
Expand Down
14 changes: 10 additions & 4 deletions test/locale/keys.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ it('Locale keys', () => {
// Improved locale object structure
expect(Object.keys(relativeTime).sort()).toEqual(['duration', 'future', 'past'].sort());
['duration', 'future', 'past'].forEach(key =>
// eslint-disable-next-line implicit-arrow-linebreak
expect(Object.keys(relativeTime[key]).sort())
.toEqual(['d', 'dd', 'ddd', 'h', 'hh', 'hhh', 'm', 'mm', 'mmm',
'M', 'MM', 'MMM', 's', 'y', 'yy', 'yyy'].sort()));
['duration', 'future', 'past'].forEach(key =>
// eslint-disable-next-line implicit-arrow-linebreak
Object.keys(relativeTime[key]).forEach(key2 =>
// eslint-disable-next-line implicit-arrow-linebreak
expect(typeof relativeTime[key][key2]).toEqual('string')))
Expand All @@ -61,12 +63,16 @@ it('Locale keys', () => {
['duration', 'future', 'past'].forEach(key =>
// eslint-disable-next-line implicit-arrow-linebreak
expect(Object.keys(relativeTime[key]).sort())
.toEqual(['d', 'h', 'm', 'M', 's', 'y'].sort()));
.toEqual(['M', 'MM', 'd', 'dd', 'h', 'hh', 'm', 'mm', 's', 'y', 'yy'].sort()));
['duration', 'future', 'past'].forEach(key =>
// eslint-disable-next-line implicit-arrow-linebreak
expect(Object.keys(relativeTime[key]).every(key2 =>
// eslint-disable-next-line implicit-arrow-linebreak
Array.isArray(relativeTime[key][key2]))).toBeTruthy())
Object.keys(relativeTime[key]).forEach((key2) => {
if (key2.length === 1) {
expect(typeof relativeTime[key][key2]).toEqual('string')
} else {
expect(Array.isArray(relativeTime[key][key2])).toBeTruthy()
}
}))
expect(typeof relativeTime.pluralRule === 'number'
|| typeof relativeTime.pluralRule === 'function').toBeTruthy()
}
Expand Down
Loading

0 comments on commit 1bb9c6b

Please sign in to comment.