├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── .eslintrc.json ├── extended-format.test.js ├── helpers │ └── index.js ├── optional-format.test.js ├── other-languages.test.js ├── timer-format.test.js └── unit-durations.test.js ├── index.js ├── index.stories.js ├── internals └── testing │ └── test-setup.js ├── messages.js ├── package-lock.json ├── package.json └── rollup.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "modules": false, 5 | "targets": "last 2 versions, ie 11" 6 | }], 7 | "@babel/react" 8 | ], 9 | "env": { 10 | "test": { 11 | "presets": ["@babel/env", "@babel/react"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "env": { 8 | "es6": true 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2020, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "rules": { 21 | "comma-dangle": ["error", { 22 | "arrays": "always-multiline", 23 | "objects": "always-multiline", 24 | "functions": "always-multiline" 25 | }], 26 | "react/display-name": 0, 27 | "react/prop-types": 0 28 | }, 29 | "settings": { 30 | "react": { 31 | "version": "16.0" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../**/*.stories.[tj]s'], 3 | addons: ['storybook-addon-intl/register'], 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { addDecorator } from "@storybook/react"; 2 | import { setIntlConfig, withIntl } from "storybook-addon-intl"; 3 | 4 | const locales = ["en", "ja"]; 5 | 6 | // Provide your messages, or you can import local locale messages files. 7 | const messages = { 8 | 'en': {}, 9 | 'ja': { 10 | 'react-intl-formatted-duration.longFormatting': '{minutes}{seconds}', 11 | 'react-intl-formatted-duration.duration': '{value}{unit}', 12 | 'react-intl-formatted-duration.daysUnit': '日', 13 | 'react-intl-formatted-duration.hoursUnit': '時', 14 | 'react-intl-formatted-duration.minutesUnit': '分', 15 | 'react-intl-formatted-duration.secondsUnit': '秒', 16 | }, 17 | }; 18 | 19 | // Set intl configuration 20 | setIntlConfig({ 21 | defaultLocale: "en", 22 | locales, 23 | getMessages: locale => messages[locale], 24 | }); 25 | 26 | // Register decorator 27 | addDecorator(withIntl); 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | git: 5 | depth: 3 6 | cache: npm 7 | notifications: 8 | email: false 9 | script: 10 | - npm run lint 11 | - npm run test:coverage 12 | after_success: 13 | - test -f ./coverage/lcov.info && npm run codecov 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 AIR en-japan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Formatted Duration 2 | 3 | [react-intl](https://github.com/yahoo/react-intl) is an amazing library providing React components and API to localize your application, however it lacks a `Duration` component. 4 | 5 | If you want to show the time it takes to do something like `1 minute` or `5 minutes` or even a simple timer `0:30` you're [out of luck](https://github.com/yahoo/react-intl/issues/77) because the ECMA committee hasn't specified the [DurationFormat](https://github.com/tc39/ecma402/issues/47) yet. 6 | 7 | This component provides a very simple abstraction that works on React (DOM), React Native and any other target environment to format simple durations. 8 | 9 | ## Usage 10 | 11 | `npm i --save react-intl-formatted-duration` 12 | 13 | ### Extended format 14 | 15 | ```js 16 | // Using React DOM 17 | import React from 'react'; 18 | import FormattedDuration from 'react-intl-formatted-duration'; 19 | 20 | export default MyComponent() { 21 | return 22 | // will render `1 minute` 23 | } 24 | ``` 25 | 26 | The default format only shows minutes and seconds. For more complex needs check the [custom format section](#Custom_format). 27 | 28 | By default the output text is wrapped in a `span`, you can specify any component you like available on your target environment: 29 | 30 | ```js 31 | // Using React Native 32 | import React from 'react'; 33 | import FormattedDuration from 'react-intl-formatted-duration'; 34 | 35 | import { Text } from 'react-native'; 36 | 37 | export default MyComponent() { 38 | return 39 | // will render `1 minute` 40 | } 41 | ``` 42 | 43 | ```js 44 | // Using styled components 45 | import React from 'react'; 46 | import FormattedDuration from 'react-intl-formatted-duration'; 47 | 48 | import styled from 'styled-components'; 49 | const Text = styled.span``; 50 | 51 | export default MyComponent() { 52 | return 53 | // will render `1 minute` 54 | } 55 | ``` 56 | 57 | #### Styling numbers 58 | 59 | If you want to style numbers differently from text you can pass a `valueComponent` 60 | 61 | ```js 62 | 63 | 64 | // renders 65 | 66 | 1 minute 30 seconds 67 | ``` 68 | 69 | Having different components is useful not only for styling. Some languages use different [numbering systems](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat). For example Japanese has full-width numbers, so you might want to render `10分` instead of `10分`, to do so you can use: 70 | 71 | ```js 72 | import React from 'react'; 73 | import { FormattedNumber } from 'react-intl'; 74 | import FormattedDuration from 'react-intl-formatted-duration'; 75 | 76 | /* 77 | You'll also need to select Japanese locale and configure the IntlProvider to use 78 | `ja-JP-u-nu-fullwide` 79 | 80 | Somewhere in you application 81 | import { IntlProvider } from 'react-intl'; 82 | 85 | */ 86 | 87 | export default MyComponent() { 88 | return 89 | // will render `10分` 90 | } 91 | ``` 92 | 93 | ### Custom format 94 | 95 | #### Hours and days 96 | 97 | By default the component only renders minutes and seconds, if you want to display hours or days you can use a custom format: 98 | 99 | ```js 100 | 101 | // will render `2 days 2 hours` 102 | 103 | 104 | // will render `50 hours` 105 | 106 | 107 | // will render `3000 minutes` 108 | } 109 | ``` 110 | 111 | Seconds is also optional and if missing, minutes will be rounded to the closed value 112 | 113 | ```js 114 | 115 | // will render `0 minutes` 116 | 117 | 118 | // will render `1 minute` 119 | 120 | 121 | // will render `1 minute` 122 | ``` 123 | 124 | The custom format can itself be localized by passing a message id instead of the actual value 125 | 126 | ```js 127 | import React from 'react'; 128 | import FormattedDuration from 'react-intl-formatted-duration'; 129 | 130 | import messages from './messages'; 131 | 132 | export default MyComponent() { 133 | return ( 134 | 138 | ); 139 | } 140 | ``` 141 | 142 | #### Unit display 143 | 144 | While `format` allows to select which units to render, `unitDisplay` allows to configure the way each unit is rendered. 145 | 146 | ```js 147 | 148 | // will render `1 minute` 149 | 150 | 151 | // will render `1 min` 152 | 153 | 154 | // will render `1m` 155 | ``` 156 | 157 | #### Timer format 158 | 159 | ```js 160 | import FormattedDuration, { TIMER_FORMAT } from 'react-intl-formatted-duration'; 161 | 162 | export default MyComponent() { 163 | return 164 | // will render `1:00` 165 | } 166 | ``` 167 | 168 | ## Localization 169 | 170 | `react-intl-formatted-duration` expects the following keys inside your translation file 171 | 172 | * `react-intl-formatted-duration.longFormatting` the default format that generates something like `1 minute 30 seconds`. It uses the values `{days}`, `{hours}`, `{minutes}` and `{seconds}`. For example you can change it to `{minutes} and {seconds}`. 173 | * `react-intl-formatted-duration.duration` the format used by the `minutes` and `seconds` variables described above. It uses the values `{value}` and `{unit}`. The default is `{value} {unit}` where `value` is a number and `{unit}` is the textual unit like `minute(s)` or `second(s)`. 174 | * `react-intl-formatted-duration.timerFormatting` format for `TIMER_FORMAT`, defaults to `{minutes}:{seconds}` where both values are numbers padded to have a minimum length of 2 characters 175 | * `react-intl-formatted-duration.daysUnit` string for formatting days, default `{value, plural, one {day} other {days}}` 176 | * `react-intl-formatted-duration.hoursUnit` string for formatting hours, default `{value, plural, one {hour} other {hours}}` 177 | * `react-intl-formatted-duration.minutesUnit` string for formatting minutes, default `{value, plural, one {minute} other {minutes}}` 178 | * `react-intl-formatted-duration.secondsUnit` string for formatting seconds, default `{value, plural, one {second} other {seconds}}` 179 | 180 | The messages for `daysUnit`, `hoursUnit`, `minutesUnit`, `secondsUnit` use the [format-js syntax](https://formatjs.io/guides/message-syntax/) and are only used when `unitDisplay` is not specified. 181 | 182 | If you're using the `extract-intl` script from [react-boilerplate](https://github.com/react-boilerplate/react-boilerplate) you can import `react-intl-formatted-duration/messages` to automatically generate the keys in your translation files. 183 | 184 | 185 | ## Upgrading from version 1.0 186 | 187 | Version `2.x` allows to use the whole power of format-js message syntax. All you need to do is remove all keys like `daysSingular`, `dayPlural` and simply use `daysUnit` with the format described above. 188 | 189 | ## Upgrading from version 2.0 190 | 191 | Version `3.x` has exactly the same API as version `2.x` but is a complete rewrite. You don't need to change your code. 192 | 193 | ## Upgrading from version 3.0 194 | 195 | Version `4.x` doesn't change any of the default behavior of version `3.x` and only contains new features. However internally it bumps the version of `intl-unofficial-duration-unit-format` from `1.x` to `3.x` which now requires `Intl.NumberFormat` to be available globally. If you've installed `react-intl` correctly, chances are you don't need to change your code. 196 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/extended-format.test.js: -------------------------------------------------------------------------------- 1 | import { text } from './helpers'; 2 | 3 | describe('Extended format', () => { 4 | it('formats duration in English with long format (react)', () => { 5 | expect(text(1)).toEqual('1 second'); 6 | expect(text(30)).toEqual('30 seconds'); 7 | expect(text(59)).toEqual('59 seconds'); 8 | expect(text(60)).toEqual('1 minute'); 9 | expect(text(61)).toEqual('1 minute 1 second'); 10 | expect(text(62)).toEqual('1 minute 2 seconds'); 11 | expect(text(120)).toEqual('2 minutes'); 12 | expect(text(121)).toEqual('2 minutes 1 second'); 13 | expect(text(150)).toEqual('2 minutes 30 seconds'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/helpers/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IntlProvider} from 'react-intl'; 3 | import {mount} from 'enzyme'; 4 | import DurationMessage from '../../index'; 5 | 6 | let currentLocale = 'en'; 7 | 8 | const messages = { 9 | en: {}, 10 | ja: { 11 | "react-intl-formatted-duration/custom-format/ja-full-test-format": "{days} {hours} {minutes} {seconds}", 12 | "react-intl-formatted-duration.duration": "{value}{unit}", 13 | "react-intl-formatted-duration.secondsUnit": "{value, plural, other {秒}}", 14 | "react-intl-formatted-duration.minutesUnit": "{value, plural, other {分}}", 15 | "react-intl-formatted-duration.hoursUnit": "{value, plural, other {時}}", 16 | "react-intl-formatted-duration.daysUnit": "{value, plural, other {日}}", 17 | }, 18 | }; 19 | 20 | export function setLocale(locale) { 21 | currentLocale = locale; 22 | } 23 | 24 | function mountWithIntl(node) { 25 | return mount(node, { 26 | wrappingComponent: IntlProvider, 27 | wrappingComponentProps: { 28 | locale: currentLocale, 29 | defaultLocale: currentLocale, 30 | messages: messages[currentLocale], 31 | }, 32 | }); 33 | } 34 | 35 | function Text(props) { 36 | return ; 37 | } 38 | 39 | export function text(value, format) { 40 | const unitDisplay = ['long', 'short', 'narrow'].includes(format) ? format : undefined; 41 | return mountWithIntl(( 42 | 48 | )).text().trim().replace(/\s+/g, ' '); 49 | } 50 | -------------------------------------------------------------------------------- /__tests__/optional-format.test.js: -------------------------------------------------------------------------------- 1 | import { text } from './helpers'; 2 | 3 | describe('Optional format', () => { 4 | it('formats duration using hours', () => { 5 | const format = '{hours} {minutes} {seconds}'; 6 | expect(text(1, format)).toEqual('1 second'); 7 | expect(text(60, format)).toEqual('1 minute'); 8 | expect(text(61, format)).toEqual('1 minute 1 second'); 9 | expect(text(3600, format)).toEqual('1 hour'); 10 | expect(text(3601, format)).toEqual('1 hour 1 second'); 11 | expect(text(3602, format)).toEqual('1 hour 2 seconds'); 12 | expect(text(3660, format)).toEqual('1 hour 1 minute'); 13 | expect(text(3661, format)).toEqual('1 hour 1 minute 1 second'); 14 | expect(text(7322, format)).toEqual('2 hours 2 minutes 2 seconds'); 15 | expect(text(90000, format)).toEqual('25 hours'); 16 | }); 17 | 18 | it('formats duration using days and ignoring seconds', () => { 19 | const format = '{days} {hours} {minutes}'; 20 | expect(text(1, format)).toEqual('0 minutes'); 21 | expect(text(60, format)).toEqual('1 minute'); 22 | expect(text(61, format)).toEqual('1 minute'); 23 | expect(text(110, format)).toEqual('2 minutes'); 24 | expect(text(3600, format)).toEqual('1 hour'); 25 | expect(text(3601, format)).toEqual('1 hour'); 26 | expect(text(3660, format)).toEqual('1 hour 1 minute'); 27 | expect(text(3661, format)).toEqual('1 hour 1 minute'); 28 | expect(text(7322, format)).toEqual('2 hours 2 minutes'); 29 | expect(text(86400, format)).toEqual('1 day'); 30 | expect(text(90000, format)).toEqual('1 day 1 hour'); 31 | expect(text(180000, format)).toEqual('2 days 2 hours'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/other-languages.test.js: -------------------------------------------------------------------------------- 1 | import {setLocale, text} from './helpers'; 2 | 3 | describe('Optional format', () => { 4 | it('formats duration using hours', () => { 5 | setLocale('ja'); 6 | 7 | const format = 'ja-full-test-format'; 8 | expect(text(1, format)).toEqual('1秒'); 9 | expect(text(60, format)).toEqual('1分'); 10 | expect(text(61, format)).toEqual('1分 1秒'); 11 | expect(text(3600, format)).toEqual('1時'); 12 | expect(text(3601, format)).toEqual('1時 1秒'); 13 | expect(text(3602, format)).toEqual('1時 2秒'); 14 | expect(text(3660, format)).toEqual('1時 1分'); 15 | expect(text(3661, format)).toEqual('1時 1分 1秒'); 16 | expect(text(7322, format)).toEqual('2時 2分 2秒'); 17 | expect(text(90000, format)).toEqual('1日 1時'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/timer-format.test.js: -------------------------------------------------------------------------------- 1 | import { text } from './helpers'; 2 | 3 | import { TIMER_FORMAT } from '../index'; 4 | 5 | describe('Timer format', () => { 6 | it('formats duration in English with timer formar', () => { 7 | expect(text(1, TIMER_FORMAT)).toEqual('0:01'); 8 | expect(text(30, TIMER_FORMAT)).toEqual('0:30'); 9 | expect(text(59, TIMER_FORMAT)).toEqual('0:59'); 10 | expect(text(60, TIMER_FORMAT)).toEqual('1:00'); 11 | expect(text(61, TIMER_FORMAT)).toEqual('1:01'); 12 | expect(text(62, TIMER_FORMAT)).toEqual('1:02'); 13 | expect(text(120, TIMER_FORMAT)).toEqual('2:00'); 14 | expect(text(121, TIMER_FORMAT)).toEqual('2:01'); 15 | expect(text(150, TIMER_FORMAT)).toEqual('2:30'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/unit-durations.test.js: -------------------------------------------------------------------------------- 1 | import { text } from './helpers'; 2 | 3 | describe('Extended format', () => { 4 | it('formats duration using CLDR unitDisplay (long)', () => { 5 | expect(text(1, 'long')).toEqual('1 second'); 6 | expect(text(30, 'long')).toEqual('30 seconds'); 7 | expect(text(59, 'long')).toEqual('59 seconds'); 8 | expect(text(60, 'long')).toEqual('1 minute'); 9 | expect(text(61, 'long')).toEqual('1 minute 1 second'); 10 | expect(text(62, 'long')).toEqual('1 minute 2 seconds'); 11 | expect(text(120, 'long')).toEqual('2 minutes'); 12 | expect(text(121, 'long')).toEqual('2 minutes 1 second'); 13 | expect(text(150, 'long')).toEqual('2 minutes 30 seconds'); 14 | }); 15 | 16 | it('formats duration using CLDR unitDisplay (short)', () => { 17 | expect(text(1, 'short')).toEqual('1 sec'); 18 | expect(text(30, 'short')).toEqual('30 sec'); 19 | expect(text(59, 'short')).toEqual('59 sec'); 20 | expect(text(60, 'short')).toEqual('1 min'); 21 | expect(text(61, 'short')).toEqual('1 min 1 sec'); 22 | expect(text(62, 'short')).toEqual('1 min 2 sec'); 23 | expect(text(120, 'short')).toEqual('2 min'); 24 | expect(text(121, 'short')).toEqual('2 min 1 sec'); 25 | expect(text(150, 'short')).toEqual('2 min 30 sec'); 26 | }); 27 | 28 | it('formats duration using CLDR unitDisplay (narrow)', () => { 29 | expect(text(1, 'narrow')).toEqual('1s'); 30 | expect(text(30, 'narrow')).toEqual('30s'); 31 | expect(text(59, 'narrow')).toEqual('59s'); 32 | expect(text(60, 'narrow')).toEqual('1m'); 33 | expect(text(61, 'narrow')).toEqual('1m 1s'); 34 | expect(text(62, 'narrow')).toEqual('1m 2s'); 35 | expect(text(120, 'narrow')).toEqual('2m'); 36 | expect(text(121, 'narrow')).toEqual('2m 1s'); 37 | expect(text(150, 'narrow')).toEqual('2m 30s'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unfortunately ReactIntl doesn't support duration yet because it's not implemented 3 | * in the ECMA specs: https://github.com/yahoo/react-intl/issues/77 4 | * 5 | * becomes 30 seconds 6 | * becomes 1 minute 7 | * becomes 2 minutes 30 seconds 8 | * `minutes` and `seconds` are translated 9 | * 10 | * becomes 0:30 11 | * becomes 1:00 12 | * becomes 2:30 13 | */ 14 | 15 | import React from 'react'; 16 | import { injectIntl } from 'react-intl'; 17 | import DurationUnitFormat from 'intl-unofficial-duration-unit-format'; 18 | 19 | export const EXTENDED_FORMAT = 'EXTENDED_FORMAT'; 20 | export const TIMER_FORMAT = 'TIMER_FORMAT'; 21 | 22 | function DurationMessage({ intl, seconds, format, textComponent, unitDisplay, valueComponent, ...otherProps }) { 23 | let actualFormat = intl.messages[`react-intl-formatted-duration/custom-format/${format || ''}`] || format; 24 | if (!format || format === EXTENDED_FORMAT) { 25 | actualFormat = intl.messages['react-intl-formatted-duration.longFormatting'] || '{minutes} {seconds}'; 26 | } 27 | if (format === TIMER_FORMAT) { 28 | actualFormat = intl.messages['react-intl-formatted-duration.timerFormatting'] || '{minutes}:{seconds}'; 29 | } 30 | let actualSytle = unitDisplay; 31 | if (!actualSytle) { 32 | actualSytle = format === TIMER_FORMAT ? DurationUnitFormat.styles.TIMER : DurationUnitFormat.styles.CUSTOM; 33 | } 34 | const parts = new DurationUnitFormat(intl.locale, { 35 | format: actualFormat, 36 | formatUnits: { 37 | [DurationUnitFormat.units.DAY]: intl.messages['react-intl-formatted-duration.daysUnit'] || '{value, plural, one {day} other {days}}', 38 | [DurationUnitFormat.units.HOUR]: intl.messages['react-intl-formatted-duration.hoursUnit'] || '{value, plural, one {hour} other {hours}}', 39 | [DurationUnitFormat.units.MINUTE]: intl.messages['react-intl-formatted-duration.minutesUnit'] || '{value, plural, one {minute} other {minutes}}', 40 | [DurationUnitFormat.units.SECOND]: intl.messages['react-intl-formatted-duration.secondsUnit'] || '{value, plural, one {second} other {seconds}}', 41 | }, 42 | formatDuration: intl.messages['react-intl-formatted-duration.duration'] || '{value} {unit}', 43 | round: true, // TODO backward compatible, add a prop to configure it 44 | style: actualSytle, 45 | }).formatToParts(seconds); 46 | 47 | const Text = textComponent || intl.textComponent; 48 | const Value = valueComponent || textComponent || intl.textComponent; 49 | return React.createElement(Text, otherProps, parts.map((token) => { 50 | if (token.type === 'literal' || token.type === 'unit') return token.value; 51 | return React.createElement(Value, { key: token.type }, token.value); 52 | })); 53 | } 54 | 55 | export default injectIntl(DurationMessage); 56 | -------------------------------------------------------------------------------- /index.stories.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import FormattedDuration, {TIMER_FORMAT} from './index'; 3 | 4 | export default { title: 'Formatted Duration' }; 5 | 6 | function withInput(defaultValue, Example) { 7 | return () => { 8 | const [seconds, setSeconds] = useState(defaultValue); 9 | return ( 10 |
11 | setSeconds(Number(evt.target.value))} /> 12 |
13 | 14 | 15 | ); 16 | }} 17 | 18 | export const defaultFormat = withInput(95, ({seconds}) => ( 19 | 20 | )); 21 | 22 | export const customFormats = withInput(180000, ({seconds}) => ( 23 | 24 |
25 | Format: {`{days} {hours} {minutes} {seconds}`} 26 |
27 | 28 |
29 |
30 | Format: {`{hours} {minutes} {seconds}`} 31 |
32 | 33 |
34 |
35 | Format: {`{minutes} {seconds}`} 36 |
37 | 38 |
39 |
40 | Format: {`{seconds}`} 41 |
42 | 43 |
44 |
45 | )); 46 | 47 | export const styles = withInput(3610, ({seconds}) => ( 48 | 49 |
50 | Unit Display: long 51 |
52 | 53 |
54 |
55 | Unit Display: short 56 |
57 | 58 |
59 |
60 | Unit Display: narrow 61 |
62 | 63 |
64 |
65 | TIMER 66 |
67 | 68 |
69 |
70 | )); 71 | -------------------------------------------------------------------------------- /internals/testing/test-setup.js: -------------------------------------------------------------------------------- 1 | // setup file 2 | import { configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DurationMessage Messages 3 | * 4 | * This contains all the text for the DurationMessage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | duration: { 10 | id: 'react-intl-formatted-duration.duration', 11 | defaultMessage: '{value} {unit}', 12 | }, 13 | longFormatting: { 14 | id: 'react-intl-formatted-duration.longFormatting', 15 | defaultMessage: '{minutes} {seconds}', 16 | }, 17 | timerFormatting: { 18 | id: 'react-intl-formatted-duration.timerFormatting', 19 | defaultMessage: '{minutes}:{seconds}', 20 | }, 21 | daysUnit: { 22 | id: 'react-intl-formatted-duration.daysUnit', 23 | defaultMessage: '{value, plural, one {day} other {days}}', 24 | }, 25 | hoursUnit: { 26 | id: 'react-intl-formatted-duration.hoursUnit', 27 | defaultMessage: '{value, plural, one {hour} other {hours}}', 28 | }, 29 | minutesUnit: { 30 | id: 'react-intl-formatted-duration.minutesUnit', 31 | defaultMessage: '{value, plural, one {minute} other {minutes}}', 32 | }, 33 | secondsUnit: { 34 | id: 'react-intl-formatted-duration.secondsUnit', 35 | defaultMessage: '{value, plural, one {second} other {seconds}}', 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-intl-formatted-duration", 3 | "version": "4.0.0", 4 | "description": "React intl component to express time duration", 5 | "keywords": [ 6 | "duration", 7 | "react", 8 | "intl", 9 | "format" 10 | ], 11 | "homepage": "https://github.com/en-japan-air/react-intl-formatted-duration", 12 | "license": "MIT", 13 | "main": "dist/bundle.js", 14 | "module": "dist/module.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/en-japan-air/react-intl-formatted-duration" 18 | }, 19 | "files": [ 20 | "dist", 21 | "index.js", 22 | "messages.js" 23 | ], 24 | "scripts": { 25 | "compile": "rollup -c rollup.config.js", 26 | "codecov": "codecov", 27 | "lint": "eslint index.js __tests__", 28 | "storybook": "start-storybook", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:watch": "jest --watchAll", 32 | "prepare": "npm run compile", 33 | "posttest": "npm run lint" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.9.0", 37 | "@babel/preset-env": "^7.9.5", 38 | "@babel/preset-react": "^7.9.4", 39 | "@storybook/addons": "5.3.18", 40 | "@storybook/react": "5.3.18", 41 | "babel-core": "^7.0.0-bridge.0", 42 | "babel-eslint": "^10.1.0", 43 | "babel-jest": "^25.3.0", 44 | "babel-loader": "8.1.0", 45 | "codecov": "3.7.1", 46 | "enzyme": "3.11.0", 47 | "enzyme-adapter-react-16": "1.15.2", 48 | "eslint": "^6.8.0", 49 | "eslint-plugin-react": "^7.19.0", 50 | "jest": "^25.3.0", 51 | "react": "16.13.1", 52 | "react-dom": "16.13.1", 53 | "react-intl": "4.3.1", 54 | "react-test-renderer": "16.13.1", 55 | "rollup": "2.6.0", 56 | "rollup-plugin-babel": "4.4.0", 57 | "rollup-plugin-commonjs": "10.1.0", 58 | "rollup-plugin-filesize": "7.0.0", 59 | "rollup-plugin-node-resolve": "5.2.0", 60 | "storybook-addon-intl": "2.4.1" 61 | }, 62 | "peerDependencies": { 63 | "react": ">= 16.0", 64 | "react-intl": ">= 4.0", 65 | "intl-messageformat": ">= 2.0" 66 | }, 67 | "jest": { 68 | "setupFilesAfterEnv": [ 69 | "/internals/testing/test-setup.js" 70 | ], 71 | "testRegex": "__tests__/.*\\.test\\.js$", 72 | "transform": { 73 | ".*": "/node_modules/babel-jest" 74 | } 75 | }, 76 | "dependencies": { 77 | "intl-unofficial-duration-unit-format": "3.0.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import pkg from './package.json'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | 7 | export default { 8 | input: 'index.js', 9 | plugins: [ 10 | resolve({ module: false }), 11 | commonjs(), 12 | babel(), 13 | filesize(), 14 | ], 15 | external: Object.keys(pkg.peerDependencies), 16 | output: [{ 17 | exports: 'named', 18 | file: 'dist/bundle.js', 19 | format: 'cjs', 20 | }, { 21 | file: 'dist/module.js', 22 | format: 'es', 23 | }], 24 | }; 25 | --------------------------------------------------------------------------------