diff --git a/build/rollup.config.js b/build/rollup.config.js
index 4d31b990e..5ff557422 100644
--- a/build/rollup.config.js
+++ b/build/rollup.config.js
@@ -7,7 +7,7 @@ module.exports = (config) => {
input: {
input,
external: [
- 'dayjs'
+ 'dayjs', 'fast-plural-rules'
],
plugins: [
babel({
@@ -21,7 +21,8 @@ module.exports = (config) => {
format: 'umd',
name: name || 'dayjs',
globals: {
- dayjs: 'dayjs'
+ dayjs: 'dayjs',
+ 'fast-plural-rules': 'fastPluralRules'
}
}
}
diff --git a/docs/en/I18n.md b/docs/en/I18n.md
index c08a1a1b9..fee9e4b7b 100644
--- a/docs/en/I18n.md
+++ b/docs/en/I18n.md
@@ -83,6 +83,14 @@ const localeObject = {
months: 'Enero_Febrero ... '.split('_'), // months Array
monthsShort: 'Jan_F'.split('_'), // OPTIONAL, short months Array, use first three letters if not provided
ordinal: n => `${n}º`, // ordinal Function (number) => return number + output
+ relativeTime: {
+ // see below
+ }
+}
+```
+
+Old template of the part of a Day.js locale Object for the RelativeTime plugin. It works well for languages which do not decline nouns and which have only one form of plural. English, for example.
+```javascript
relativeTime: { // relative time format strings, keep %s %d as the same
future: 'in %s', // e.g. in 2 hours, %s been replaced with 2hours
past: '%s ago',
@@ -98,9 +106,58 @@ const localeObject = {
y: 'a year',
yy: '%d years'
}
-}
```
+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 multiple plural forms. 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
+ // Using 3 plural forms in Slavic languages
+ duration: {
+ // 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',
+ 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',
+ 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 keys with single-letter time units point to a string shown for a single value, usually without any number. The keys with two-letter time units point to arrays with plural forms. Before you work on a new localization, make yourself familiar with [plural rules and plural forms for the target language](https://github.com/prantlf/fast-plural-rules/blob/master/docs/languages.md#supported-languages). You will find more information about [plural rules](https://github.com/prantlf/fast-plural-rules/blob/master/docs/design.md#plural-rules) and [plural forms](https://github.com/prantlf/fast-plural-rules/blob/master/docs/design.md#plural-forms) in the [design](https://github.com/prantlf/fast-plural-rules/blob/master/docs/design.md#design-concepts) of the library [fast-plural-rules](https://github.com/prantlf/fast-plural-rules#fast-plural-rules) used for the grammatically correct localization of expressions with cardinal numbers.
+
Template of a Day.js locale file.
```javascript
import dayjs from 'dayjs'
diff --git a/docs/en/Plugin.md b/docs/en/Plugin.md
index 2b8221655..2e3d2920f 100644
--- a/docs/en/Plugin.md
+++ b/docs/en/Plugin.md
@@ -113,6 +113,16 @@ Returns the `string` of relative time to X.
| 11 months to 17months | y | a year ago |
| 18 months+ | yy | 2 years ago ... 20 years ago |
+#### Installation
+
+This plugin has a dependency on the [`fast-plural-rules`](https://www.npmjs.com/package/fast-plural-rules) NPM module. If you are going to use it on a web page directly, add its script to your section of `
+
+
+```
+
### IsLeapYear
- IsLeapYear adds `.isLeapYear` API to returns a `boolean` indicating whether the `Dayjs`'s year is a leap year or not.
diff --git a/package-lock.json b/package-lock.json
index 6394ec30e..0ebe06ec5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1607,7 +1607,7 @@
},
"chalk": {
"version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@@ -1696,7 +1696,7 @@
},
"babel-plugin-istanbul": {
"version": "4.1.6",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz",
+ "resolved": "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz",
"integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==",
"dev": true,
"requires": {
@@ -1714,7 +1714,7 @@
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
+ "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
"integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
"dev": true
},
@@ -2165,7 +2165,7 @@
},
"browserify-aes": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@@ -2201,7 +2201,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@@ -3013,7 +3013,7 @@
},
"create-hash": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@@ -3026,7 +3026,7 @@
},
"create-hmac": {
"version": "1.1.7",
- "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@@ -3505,7 +3505,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
- "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@@ -3580,7 +3580,7 @@
},
"duplexer": {
"version": "0.1.1",
- "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+ "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
@@ -4026,7 +4026,7 @@
},
"load-json-file": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
"dev": true,
"requires": {
@@ -4644,6 +4644,11 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
+ "fast-plural-rules": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/fast-plural-rules/-/fast-plural-rules-0.0.1.tgz",
+ "integrity": "sha512-0Cxx7LaH7+dNJEBozlisCxqaN5g68VTFT9PyLeFGBHmkPnQ3e46zss+r8pRC94KpzPlitL6m33GVdbMIDiUgqg=="
+ },
"fastparse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz",
@@ -5936,7 +5941,7 @@
},
"http-errors": {
"version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true,
"requires": {
@@ -6274,7 +6279,7 @@
},
"is-builtin-module": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
"dev": true,
"requires": {
@@ -7974,7 +7979,7 @@
},
"load-json-file": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"dev": true,
"requires": {
@@ -8601,7 +8606,7 @@
},
"minimist": {
"version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
@@ -8646,7 +8651,7 @@
},
"mkdirp": {
"version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
@@ -9253,7 +9258,7 @@
},
"parse-asn1": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+ "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"dev": true,
"requires": {
@@ -10339,7 +10344,7 @@
},
"readable-stream": {
"version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -11008,7 +11013,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@@ -11186,7 +11191,7 @@
},
"sha.js": {
"version": "2.4.11",
- "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
@@ -12105,7 +12110,7 @@
},
"through": {
"version": "2.3.8",
- "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
@@ -12792,7 +12797,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@@ -12848,7 +12853,7 @@
},
"request": {
"version": "2.85.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz",
+ "resolved": "http://registry.npmjs.org/request/-/request-2.85.0.tgz",
"integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==",
"dev": true,
"requires": {
@@ -13084,7 +13089,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
diff --git a/package.json b/package.json
index 25aaee9f4..9e07e3c30 100644
--- a/package.json
+++ b/package.json
@@ -82,5 +82,7 @@
"size-limit": "^0.18.0",
"typescript": "^2.8.3"
},
- "dependencies": {}
+ "dependencies": {
+ "fast-plural-rules": "^0.0.1"
+ }
}
diff --git a/src/locale/cs.js b/src/locale/cs.js
new file mode 100644
index 000000000..8c4ae70e0
--- /dev/null
+++ b/src/locale/cs.js
@@ -0,0 +1,54 @@
+import dayjs from 'dayjs'
+
+const locale = {
+ name: 'cs',
+ weekdays: 'neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota'.split('_'),
+ months: 'leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec'.split('_'),
+ ordinal: n => `${n}.`,
+ relativeTime: {
+ // Using 3 plural forms for 1, 2-4, 5-
+ duration: {
+ 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',
+ 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',
+ 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']
+ }
+ }
+}
+
+dayjs.locale(locale, null, true)
+
+export default locale
diff --git a/src/locale/ru.js b/src/locale/ru.js
index e0641017b..ee9da6d62 100644
--- a/src/locale/ru.js
+++ b/src/locale/ru.js
@@ -5,19 +5,46 @@ const locale = {
weekdays: 'воскресенье_понедельник_вторник_среда_четверг_пятница_суббота'.split('_'),
months: 'январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь'.split('_'),
relativeTime: {
- future: 'через %s',
- past: '%s назад',
- s: 'несколько секунд',
- m: 'минута',
- mm: '%d минут',
- h: 'час',
- hh: '%d часов',
- d: 'день',
- dd: '%d дней',
- M: 'месяц',
- MM: '%d месяцев',
- y: 'год',
- yy: '%d лет'
+ // Using 3 plural forms for 1 and x1, 2-4 and x2-4, 5-
+ 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 лет назад']
+ }
},
ordinal: n => n
}
diff --git a/src/plugin/relativeTime/index.js b/src/plugin/relativeTime/index.js
index 1ac51e431..491931a35 100644
--- a/src/plugin/relativeTime/index.js
+++ b/src/plugin/relativeTime/index.js
@@ -1,3 +1,5 @@
+import { getPluralFormForCardinalByLocale } from 'fast-plural-rules'
+
import * as C from '../../constant'
export default (o, c, d) => {
@@ -17,8 +19,114 @@ export default (o, c, d) => {
y: 'a year',
yy: '%d years'
}
+ // Upgrades the original locale format with the single plural only
+ // {
+ // future: '...', past: '..,',
+ // s: '...', m: '...', mm: '...'
+ // }
+ function upgradeSimpleLocale(loc) {
+ // Save wrapper expressions with prepositions
+ const { future, past } = loc
+ // Prepare localized expressions for durations (neither future nor past)
+ const durations = Object.keys(loc).reduce((result, key) => {
+ const kl = key.length
+ // Skip entries in the locale, which do not format numerals (future and past)
+ if (kl <= 2) {
+ // 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]
+ if (kl === 1) {
+ result[key] = text
+ // Insert singular for objects with plurals declared before singulars
+ const key2 = key + key
+ let plurals = result[key2]
+ if (!plurals) {
+ plurals = result[key2] = [] // eslint-disable-line no-multi-assign
+ }
+ plurals.unshift(text)
+ } else {
+ // Append plural for objects with plurals declared after singulars
+ let plurals = result[key]
+ if (!plurals) {
+ plurals = result[key] = [] // eslint-disable-line no-multi-assign
+ }
+ plurals.push(text)
+ }
+ }
+ // Remove the original locale entry; the original locale object needs
+ // to be retained to prevent upgrading on every formatting call
+ delete loc[key]
+ return result
+ }, {})
+ // Prepare localized expressions for future and past
+ const futures = {}
+ const pasts = {}
+ Object.keys(durations).forEach((key) => {
+ 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
+ }
+ // Upgrade the improved, but not the final version of the localization,
+ // which supports two plurals by keys with two and three letters
+ // {
+ // duration: { s: '...', m: '...', mm: '...', mmm: '...' },
+ // future: { ... }, past: { ... }
+ // }
+ function upgradeImprovedLocale(loc) {
+ // Put one, two and three lettered strings to an array
+ function convertPlurals(object) {
+ return Object.keys(object).reduce((result, key) => {
+ const kl = key.length
+ 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)
+ }
+ // Upgrades old locale format to provide compatibility with older
+ // localizations; the grammar may not be correct for fusional languages
+ // {
+ // duration: { s: '...', m: '...', mm: ['...', '...', ...] },
+ // future: { ... }, past: { ... }
+ // }
+ function upgradeLocale(loc) {
+ // Do not upgrade already upgraded locales
+ if (loc.s) {
+ upgradeSimpleLocale(loc)
+ } else if (typeof loc.duration.mm === 'string') {
+ upgradeImprovedLocale(loc)
+ }
+ }
const fromTo = (input, withoutSuffix, instance, isFrom) => {
- const loc = instance.$locale().relativeTime
+ const locale = instance.$locale()
+ const locs = locale.relativeTime
const T = [
{ l: 's', r: 44, d: C.S },
{ l: 'm', r: 89 },
@@ -36,21 +144,43 @@ export default (o, c, d) => {
let result
let out
+ upgradeLocale(locs)
+
for (let i = 0; i < Tl; i += 1) {
const t = T[i]
- if (t.d) {
+ const unit = t.d
+ if (unit) {
result = isFrom
- ? d(input).diff(instance, t.d, true)
- : instance.diff(input, t.d, true)
+ ? d(input).diff(instance, unit, true)
+ : instance.diff(input, unit, true)
}
const abs = Math.ceil(Math.abs(result))
- if (abs <= t.r || !t.r) {
- out = loc[t.l].replace('%d', abs)
+ const limit = t.r
+ if (abs <= limit || !limit) {
+ let loc
+ // Use the proper source of localization expressions depending
+ // on the requested expression - just duration, future or past
+ if (withoutSuffix) {
+ loc = locs.duration
+ } else if (result > 0) {
+ loc = locs.future
+ } else {
+ loc = locs.past
+ }
+ const key = t.l
+ 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
+ const pluralForms = loc[key]
+ const pluralForm = getPluralFormForCardinalByLocale(locale.name, abs)
+ out = pluralForms[pluralForm].replace('%d', abs)
+ }
break
}
}
- if (withoutSuffix) return out
- return ((result > 0) ? loc.future : loc.past).replace('%s', out)
+ return out
}
proto.to = function (input, withoutSuffix) {
return fromTo(input, withoutSuffix, this, true)
diff --git a/test/locale/keys.test.js b/test/locale/keys.test.js
index 68219c751..3a1f17cdb 100644
--- a/test/locale/keys.test.js
+++ b/test/locale/keys.test.js
@@ -37,9 +37,43 @@ it('Locale keys', () => {
expect(ordinal(3)).toEqual(expect.anything())
expect(dayjs().locale(name).$locale().name).toBe(name)
if (relativeTime) {
- expect(Object.keys(relativeTime).sort()).toEqual(['d', 'dd', 'future', 'h', 'hh', 'm', 'mm', 'M', 'MM',
- 'past', 's', 'y', 'yy']
- .sort())
+ if (relativeTime.s) {
+ // Old locale object structure
+ expect(Object.keys(relativeTime).sort())
+ .toEqual(['d', 'dd', 'future', 'h', 'hh', 'm', 'mm', 'M', 'MM', 'past', 's', 'y', 'yy'].sort())
+ expect(Object.keys(relativeTime).every(key =>
+ // eslint-disable-next-line implicit-arrow-linebreak
+ typeof relativeTime[key] === 'string')).toBeTruthy()
+ } else if (relativeTime.duration.mmm) {
+ // 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')))
+ } else {
+ // Ultimate 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(['M', 'MM', 'd', 'dd', 'h', 'hh', 'm', 'mm', 's', 'y', 'yy'].sort()));
+ ['duration', 'future', 'past'].forEach(key =>
+ // eslint-disable-next-line implicit-arrow-linebreak
+ 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()
+ }
+ }))
+ }
}
})
})
diff --git a/test/plugin/relativeTime.test.js b/test/plugin/relativeTime.test.js
index 2bc4a896e..1d9b405b3 100644
--- a/test/plugin/relativeTime.test.js
+++ b/test/plugin/relativeTime.test.js
@@ -2,6 +2,7 @@ import MockDate from 'mockdate'
import moment from 'moment'
import dayjs from '../../src'
import relativeTime from '../../src/plugin/relativeTime'
+import '../../src/locale/cs'
dayjs.extend(relativeTime)
@@ -13,6 +14,109 @@ afterEach(() => {
MockDate.reset()
})
+it('Upgrades old locale objects', () => {
+ const old = {
+ name: 'old-relativeTime',
+ relativeTime: {
+ future: 'in %s',
+ past: '%s ago',
+ s: 'a few seconds',
+ m: 'a minute',
+ mm: '%d minutes',
+ hh: '%d hours',
+ h: 'an hour',
+ d: 'a day',
+ dd: '%d days',
+ M: 'a month',
+ MM: '%d months',
+ y: 'a year',
+ yy: '%d years'
+ }
+ }
+ dayjs.locale(old, null, true)
+ const locale = dayjs(undefined, { locale: 'old-relativeTime' }).$locale()
+ // Call the plugin to upgrade the locale structure on the fly
+ dayjs(undefined, { locale: 'old-relativeTime' }).fromNow()
+ // The locale has been upgraded to the new locale structure
+ expect(locale.relativeTime.s).toBeUndefined()
+ expect(typeof locale.relativeTime.duration).toEqual('object')
+ expect(typeof locale.relativeTime.duration.m).toEqual('string')
+ expect(Array.isArray([locale.relativeTime.duration.mm])).toBeTruthy()
+ expect(locale.relativeTime.duration.mm.length).toEqual(2)
+ expect(typeof locale.relativeTime.duration.mm[0]).toEqual('string')
+ expect(typeof locale.relativeTime.duration.mm[1]).toEqual('string')
+})
+
+it('Upgrades improved locale objects', () => {
+ const improved = {
+ name: 'improved-relativeTime',
+ relativeTime: {
+ duration: {
+ s: 'několik sekund',
+ m: 'minuta',
+ mm: '%d minuty',
+ mmm: '%d minut',
+ h: 'hodina',
+ hh: '%d hodiny',
+ hhh: '%d hodin',
+ d: 'den',
+ dd: '%d dny',
+ ddd: '%d dní',
+ M: 'měsíc',
+ MM: '%d měsíce',
+ MMM: '%d měsícú',
+ y: 'rok',
+ yy: '%d roky',
+ yyy: '%d let'
+ },
+ future: {
+ s: 'za několik sekund',
+ m: 'za minutu',
+ mm: 'za %d minuty',
+ mmm: 'za %d minut',
+ h: 'za hodinu',
+ hh: 'za %d hodiny',
+ hhh: 'za %d hodin',
+ d: 'zítra',
+ dd: 'za %d dny',
+ ddd: 'za %d dní',
+ M: 'za měsíc',
+ MM: 'za %d měsíce',
+ MMM: 'za %d měsícú',
+ y: 'za rok',
+ yy: 'za %d roky',
+ yyy: 'za %d let'
+ },
+ past: {
+ s: 'před několika sekundami',
+ m: 'před minutou',
+ mm: 'před %d minutami',
+ mmm: 'před %d minutami',
+ h: 'před hodinou',
+ hh: 'před %d hodinami',
+ hhh: 'před %d hodinami',
+ d: 'včera',
+ dd: 'před %d dny',
+ ddd: 'před %d dny',
+ M: 'před měsícem',
+ MM: 'před %d měsíci',
+ MMM: 'před %d měsíci',
+ y: 'vloni',
+ yy: 'před %d roky',
+ yyy: 'před %d lety'
+ }
+ }
+ }
+ dayjs.locale(improved, null, true)
+ const locale = dayjs(undefined, { locale: 'improved-relativeTime' }).$locale()
+ // Call the plugin to upgrade the locale structure on the fly
+ dayjs(undefined, { locale: 'improved-relativeTime' }).fromNow()
+ // The locale has been upgraded to the new locale structure
+ expect(typeof locale.relativeTime.duration.m).toEqual('string')
+ expect(Array.isArray([locale.relativeTime.duration.mm])).toBeTruthy()
+ expect(typeof locale.relativeTime.duration.mm[0]).toEqual('string')
+})
+
it('Time from X', () => {
const T = [
[0, 'second'], // a few seconds
@@ -52,7 +156,6 @@ it('Time from now', () => {
expect(dayjs().fromNow(true)).toBe(moment().fromNow(true))
})
-
it('Time to now', () => {
expect(dayjs().toNow()).toBe(moment().toNow())
expect(dayjs().toNow(true)).toBe(moment().toNow(true))
@@ -64,3 +167,15 @@ it('Time to X', () => {
// past date
expect(dayjs().to(dayjs().subtract(3, 'year'))).toBe(moment().to(moment().subtract(3, 'year')))
})
+
+it('Makes use of a singular rule', () => {
+ expect(dayjs(undefined).from(dayjs().subtract(1, 'minutes'))).toBe('in a minute')
+})
+
+it('Makes use of a numbered plural rule', () => {
+ expect(dayjs(undefined, { locale: 'cs' }).from(dayjs().subtract(30, 'minutes'))).toBe('za 30 minut')
+})
+
+it('Makes use of a plural rule provided as a custom function', () => {
+ expect(dayjs().from(dayjs().subtract(30, 'minutes'))).toBe('in 30 minutes')
+})