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') +})