From 26a8b9a38ef5621d5055fd185ca3a88b5a8804ca Mon Sep 17 00:00:00 2001 From: Gergely Nemeth Date: Thu, 28 Feb 2019 13:43:03 -0800 Subject: [PATCH] feat(i18n): add i18n support (#951) --- documentation-site/components/code.js | 13 +- documentation-site/components/json.js | 19 ++ .../getting-started/internationalization.mdx | 31 +++ documentation-site/routes.js | 4 + .../examples/internationalization/example.js | 17 ++ package.json | 1 + src/accordion/locale.js | 19 ++ src/accordion/panel.js | 57 +++-- .../__snapshots__/breadcrumbs.test.js.snap | 2 +- src/breadcrumbs/__tests__/breadcrumbs.test.js | 7 +- src/breadcrumbs/breadcrumbs.js | 25 +- src/breadcrumbs/locale.js | 17 ++ src/breadcrumbs/types.js | 5 +- .../__tests__/button-group.test.js | 2 +- src/button-group/button-group.js | 30 ++- src/button-group/locale.js | 17 ++ .../styled-components.test.js.snap | 30 +-- src/datepicker/calendar-header.js | 26 +- src/datepicker/datepicker.js | 111 ++++---- src/datepicker/locale.js | 24 ++ src/file-uploader/file-uploader.js | 241 +++++++++--------- src/file-uploader/locale.js | 25 ++ src/index.js | 2 + .../styled-components.test.js.snap | 48 ++-- src/locale/en_US.js | 31 +++ src/locale/index.js | 25 ++ src/locale/types.js | 29 +++ src/modal/locale.js | 17 ++ src/modal/modal.js | 79 +++--- src/pagination/locale.js | 21 ++ src/pagination/pagination.js | 139 +++++----- src/pagination/stateful-pagination.js | 5 - src/pagination/types.js | 4 +- .../__snapshots__/select.test.js.snap | 8 - src/select/default-props.js | 2 - src/select/locale.js | 19 ++ src/select/select.js | 63 +++-- src/select/types.js | 4 +- src/toast/locale.js | 17 ++ src/toast/toast.js | 51 ++-- yarn.lock | 5 + 41 files changed, 860 insertions(+), 432 deletions(-) create mode 100644 documentation-site/components/json.js create mode 100644 documentation-site/pages/getting-started/internationalization.mdx create mode 100644 documentation-site/static/examples/internationalization/example.js create mode 100644 src/accordion/locale.js create mode 100644 src/breadcrumbs/locale.js create mode 100644 src/button-group/locale.js create mode 100644 src/datepicker/locale.js create mode 100644 src/file-uploader/locale.js create mode 100644 src/locale/en_US.js create mode 100644 src/locale/index.js create mode 100644 src/locale/types.js create mode 100644 src/modal/locale.js create mode 100644 src/pagination/locale.js create mode 100644 src/select/locale.js create mode 100644 src/toast/locale.js diff --git a/documentation-site/components/code.js b/documentation-site/components/code.js index 0b7e51324d..b58b278326 100644 --- a/documentation-site/components/code.js +++ b/documentation-site/components/code.js @@ -7,6 +7,7 @@ LICENSE file in the root directory of this source tree. // @flow import * as React from 'react'; import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; +import {Block} from 'baseui/block'; type PropsT = { children: string, @@ -14,9 +15,15 @@ type PropsT = { }; const Code = (props: PropsT) => ( - - {props.children} - + + + {props.children} + + ); Code.defaultProps = { diff --git a/documentation-site/components/json.js b/documentation-site/components/json.js new file mode 100644 index 0000000000..a8571db0f6 --- /dev/null +++ b/documentation-site/components/json.js @@ -0,0 +1,19 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow +import * as React from 'react'; +import Code from './code.js'; + +type PropsT = { + src: string, +}; + +const JSONViewer = (props: PropsT) => ( + {JSON.stringify(props.src, null, 2)} +); + +export default JSONViewer; diff --git a/documentation-site/pages/getting-started/internationalization.mdx b/documentation-site/pages/getting-started/internationalization.mdx new file mode 100644 index 0000000000..8b2e530e49 --- /dev/null +++ b/documentation-site/pages/getting-started/internationalization.mdx @@ -0,0 +1,31 @@ + + +import Example from '../../components/example'; +import Layout from '../../components/layout'; +import JSON from '../../components/json'; +import en_US from 'baseui/locale/en_US.js'; + +import InternationalizationExample from 'examples/internationalization/example.js'; + +export default Layout; + +# Internationalization + +Base UI supports English as the default language. Following the instructions below, you +can use other languages too. + + + + + +## The shape of the locale file + + + +If you'd like to contribute a locale, please send a Pull Request based +on the [en_US](https://github.com/uber-web/baseui/tree/master/src/locale) locale. diff --git a/documentation-site/routes.js b/documentation-site/routes.js index d96a3a63eb..3cd1b28bdd 100644 --- a/documentation-site/routes.js +++ b/documentation-site/routes.js @@ -36,6 +36,10 @@ const routes = [ text: 'Comparison with other component libraries', path: '/getting-started/comparison', }, + { + text: 'Internationalization', + path: '/getting-started/internationalization', + }, ], }, { diff --git a/documentation-site/static/examples/internationalization/example.js b/documentation-site/static/examples/internationalization/example.js new file mode 100644 index 0000000000..dd7725bb0d --- /dev/null +++ b/documentation-site/static/examples/internationalization/example.js @@ -0,0 +1,17 @@ +import React from 'react'; +import {LocaleProvider} from 'baseui'; +import {StatefulPagination} from 'baseui/pagination'; + +const localeOverrideHu = { + pagination: { + next: 'Következő', + prev: 'Előző', + preposition: ' ', + }, +}; + +export default () => ( + + + +); diff --git a/package.json b/package.json index aedd817c5c..586beb159b 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "dependencies": { "date-fns": "2.0.0-alpha.27", "focus-trap": "^4.0.2", + "just-extend": "^4.0.2", "memoize-one": "^5.0.0", "popper.js": "^1.14.3", "react-dropzone": "^9.0.0", diff --git a/src/accordion/locale.js b/src/accordion/locale.js new file mode 100644 index 0000000000..036470aa7e --- /dev/null +++ b/src/accordion/locale.js @@ -0,0 +1,19 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type AccordionLocaleT = {| + collapse: string, + expand: string, +|}; + +const locale = { + collapse: 'Collapse', + expand: 'Expand', +}; + +export default locale; diff --git a/src/accordion/panel.js b/src/accordion/panel.js index 5c5ae0af2c..15d5b2f835 100644 --- a/src/accordion/panel.js +++ b/src/accordion/panel.js @@ -6,6 +6,7 @@ LICENSE file in the root directory of this source tree. */ // @flow import * as React from 'react'; +import {LocaleContext} from '../locale/index.js'; import {getOverrides, mergeOverrides} from '../helpers/overrides.js'; import { Plus as PlusIcon, @@ -91,31 +92,37 @@ class Panel extends React.Component { ); const ToggleIconComponent = expanded ? CheckIndeterminateIcon : PlusIcon; return ( - -
- {title} - -
- - {children} - -
+ + {locale => ( + +
+ {title} + +
+ + {children} + +
+ )} +
); } } diff --git a/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap b/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap index acd3b5207d..2537b435cd 100644 --- a/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap +++ b/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap @@ -2,7 +2,7 @@ exports[`Breadcrumbs displays separators in correct positions 1`] = ` { it('applies correct accessibility attributes to root element', () => { + const ariaLabel = 'Breadcrumbs navigation'; const example = shallow( - + Parent Page Sub-Parent Page Current Page , ); - expect(example).toHaveProp('aria-label', 'Breadcrumbs navigation'); + expect(example).toHaveProp('aria-label', ariaLabel); }); it('displays separators in correct positions', () => { diff --git a/src/breadcrumbs/breadcrumbs.js b/src/breadcrumbs/breadcrumbs.js index 5af6951d08..17bbec41b8 100644 --- a/src/breadcrumbs/breadcrumbs.js +++ b/src/breadcrumbs/breadcrumbs.js @@ -9,12 +9,16 @@ LICENSE file in the root directory of this source tree. import React, {Children} from 'react'; +import {LocaleContext} from '../locale/index.js'; import type {BreadcrumbsPropsT} from './types.js'; +import type {BreadcrumbLocaleT} from './locale.js'; import {StyledRoot, StyledSeparator, StyledIcon} from './styled-components.js'; import {getOverrides} from '../helpers/overrides.js'; -function Breadcrumbs({children, overrides = {}}: BreadcrumbsPropsT) { - const numChildren = Children.count(children); +type LocaleT = {|locale?: BreadcrumbLocaleT|}; +export function BreadcrumbsRoot(props: {|...BreadcrumbsPropsT, ...LocaleT|}) { + const {overrides = {}} = props; + const numChildren = Children.count(props.children); const childrenWithSeparators = []; const [Root, baseRootProps] = getOverrides(overrides.Root, StyledRoot); @@ -24,7 +28,7 @@ function Breadcrumbs({children, overrides = {}}: BreadcrumbsPropsT) { StyledSeparator, ); - Children.forEach(children, (child, index) => { + Children.forEach(props.children, (child, index) => { childrenWithSeparators.push(child); if (index !== numChildren - 1) { @@ -37,12 +41,25 @@ function Breadcrumbs({children, overrides = {}}: BreadcrumbsPropsT) { }); return ( - + {childrenWithSeparators} ); } +function Breadcrumbs(props: BreadcrumbsPropsT) { + return ( + + {locale => } + + ); +} + Breadcrumbs.defaultProps = { overrides: {}, }; diff --git a/src/breadcrumbs/locale.js b/src/breadcrumbs/locale.js new file mode 100644 index 0000000000..097dacf63d --- /dev/null +++ b/src/breadcrumbs/locale.js @@ -0,0 +1,17 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type BreadcrumbLocaleT = {| + ariaLabel: string, +|}; + +const locale = { + ariaLabel: 'Breadcrumbs navigation', +}; + +export default locale; diff --git a/src/breadcrumbs/types.js b/src/breadcrumbs/types.js index 960a05756c..a3dce0ba86 100644 --- a/src/breadcrumbs/types.js +++ b/src/breadcrumbs/types.js @@ -18,10 +18,11 @@ export type OverridesT = { Icon?: OverrideT<*>, }; -export type BreadcrumbsPropsT = { +export type BreadcrumbsPropsT = {| children?: Node, overrides?: OverridesT, -}; + ariaLabel?: string, +|}; export type StyledRootPropsT = { $theme: ThemeT, diff --git a/src/button-group/__tests__/button-group.test.js b/src/button-group/__tests__/button-group.test.js index e335dc5366..a3df13a72d 100644 --- a/src/button-group/__tests__/button-group.test.js +++ b/src/button-group/__tests__/button-group.test.js @@ -11,7 +11,7 @@ import {shallow} from 'enzyme'; import {Button} from '../../button/index.js'; -import {ButtonGroup} from '../index.js'; +import {ButtonGroupRoot as ButtonGroup} from '../button-group.js'; function buildSimpleWrapper(props = {}) { return shallow( diff --git a/src/button-group/button-group.js b/src/button-group/button-group.js index 9cd7866228..a952ca46c7 100644 --- a/src/button-group/button-group.js +++ b/src/button-group/button-group.js @@ -10,9 +10,11 @@ import React from 'react'; import {KIND, SIZE, SHAPE} from '../button/index.js'; import {getOverrides} from '../helpers/overrides.js'; +import {LocaleContext} from '../locale/index.js'; import {StyledRoot} from './styled-components.js'; import type {PropsT} from './types.js'; +import type {ButtonGroupLocaleT} from './locale.js'; function isSelected(selected, index) { if (!Array.isArray(selected) && typeof selected !== 'number') { @@ -49,12 +51,18 @@ function getBorderRadii(index, length) { }; } -export default function ButtonGroup(props: PropsT) { +type LocaleT = {|locale?: ButtonGroupLocaleT|}; +export function ButtonGroupRoot(props: {|...PropsT, ...LocaleT|}) { const {overrides = {}} = props; const [Root, rootProps] = getOverrides(overrides.Root, StyledRoot); return ( - + {React.Children.map(props.children, (child, index) => { if (!React.isValidElement(child)) { return null; @@ -78,7 +86,9 @@ export default function ButtonGroup(props: PropsT) { } }, overrides: { - BaseButton: {style: getBorderRadii(index, props.children.length)}, + BaseButton: { + style: getBorderRadii(index, props.children.length), + }, ...child.props.overrides, }, shape: props.shape, @@ -89,8 +99,20 @@ export default function ButtonGroup(props: PropsT) { ); } +// The wrapper component below was created to continue to support enzyme tests for the ButtonGroup +// component. Enzyme at the moment does not support React context @ 16.3. To get around the limitation +// in enzyme, we create a wrapper around the core ButtonGroup and pass context as a prop. In our tests, +// only ButtonGroupRoot will be tested. +// https://github.com/airbnb/enzyme/issues/1908#issuecomment-439747826 +export default function ButtonGroup(props: PropsT) { + return ( + + {locale => } + + ); +} + ButtonGroup.defaultProps = { - ariaLabel: 'button group', disabled: false, onClick: () => {}, shape: SHAPE.default, diff --git a/src/button-group/locale.js b/src/button-group/locale.js new file mode 100644 index 0000000000..3d68d8b31a --- /dev/null +++ b/src/button-group/locale.js @@ -0,0 +1,17 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type ButtonGroupLocaleT = {| + ariaLabel: string, +|}; + +const locale = { + ariaLabel: 'button group', +}; + +export default locale; diff --git a/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap b/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap index 9873c0b813..2f06d3ac12 100644 --- a/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap +++ b/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap @@ -9,6 +9,21 @@ Object { } `; +exports[`Component styled components Basic render: StyledCalendarHeader has correct styles 1`] = ` +Object { + "alignItems": "center", + "backgroundColor": "$theme.colors.primary", + "borderRadius": "$theme.borders.radius200 $theme.borders.radius200 0 0", + "color": "$theme.colors.white", + "display": "flex", + "justifyContent": "space-between", + "paddingBottom": "$theme.sizing.scale500", + "paddingLeft": "$theme.sizing.scale600", + "paddingRight": "$theme.sizing.scale600", + "paddingTop": "$theme.sizing.scale500", +} +`; + exports[`Component styled components Basic render: StyledDay has correct styles 1`] = ` Object { ":first-child": Object { @@ -44,21 +59,6 @@ Object { } `; -exports[`Component styled components Basic render: StyledCalendarHeader has correct styles 1`] = ` -Object { - "alignItems": "center", - "backgroundColor": "$theme.colors.primary", - "borderRadius": "$theme.borders.radius200 $theme.borders.radius200 0 0", - "color": "$theme.colors.white", - "display": "flex", - "justifyContent": "space-between", - "paddingBottom": "$theme.sizing.scale500", - "paddingLeft": "$theme.sizing.scale600", - "paddingRight": "$theme.sizing.scale600", - "paddingTop": "$theme.sizing.scale500", -} -`; - exports[`Component styled components Basic render: StyledMonth has correct styles 1`] = ` Object { "display": "inline-block", diff --git a/src/datepicker/calendar-header.js b/src/datepicker/calendar-header.js index 0404686e91..134787b6d4 100644 --- a/src/datepicker/calendar-header.js +++ b/src/datepicker/calendar-header.js @@ -8,6 +8,7 @@ LICENSE file in the root directory of this source tree. import * as React from 'react'; import {ArrowLeft, ArrowRight} from '../icon/index.js'; import {Select} from '../select/index.js'; +import {LocaleContext} from '../locale/index.js'; import { StyledCalendarHeader, StyledPrevButton, @@ -27,6 +28,7 @@ import { import {getOverrides, mergeOverrides} from '../helpers/overrides.js'; import type {HeaderPropsT} from './types.js'; import type {SharedStylePropsT} from '../select/types.js'; +import type {LocaleT} from '../locale/types.js'; const navBtnStyle = ({$theme}) => ({ cursor: 'pointer', @@ -59,7 +61,7 @@ export default class CalendarHeader extends React.Component { this.props.onMonthChange({date: subMonths(this.props.date, 1)}); }; - renderPreviousMonthButton = () => { + renderPreviousMonthButton = ({locale}: {locale: LocaleT}) => { const {date, overrides = {}} = this.props; const allPrevDaysDisabled = monthDisabledBefore(date, this.props); @@ -81,7 +83,7 @@ export default class CalendarHeader extends React.Component { } return ( { ); }; - renderNextMonthButton = () => { + renderNextMonthButton = ({locale}: {locale: LocaleT}) => { const {date, overrides = {}} = this.props; const allNextDaysDisabled = monthDisabledAfter(date, this.props); @@ -126,7 +128,7 @@ export default class CalendarHeader extends React.Component { } return ( { StyledCalendarHeader, ); return ( - - {this.renderPreviousMonthButton()} - {this.renderMonthDropdown()} - {this.renderYearDropdown()} - {this.renderNextMonthButton()} - + + {locale => ( + + {this.renderPreviousMonthButton({locale})} + {this.renderMonthDropdown()} + {this.renderYearDropdown()} + {this.renderNextMonthButton({locale})} + + )} + ); } } diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index d2d9716230..2cf983923f 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -11,6 +11,7 @@ import {Popover, PLACEMENT} from '../popover/index.js'; import Calendar from './calendar.js'; import {formatDate} from './utils/index.js'; import {getOverrides} from '../helpers/overrides.js'; +import {LocaleContext} from '../locale/index.js'; import type {DatepickerPropsT} from './types.js'; export default class Datepicker extends React.Component< @@ -23,7 +24,7 @@ export default class Datepicker extends React.Component< }, > { static defaultProps = { - 'aria-label': 'Select a date', + 'aria-label': null, 'aria-labelledby': null, 'aria-describedby': 'datepicker--screenreader--message--input', disabled: false, @@ -138,58 +139,64 @@ export default class Datepicker extends React.Component< overrides.Popover, Popover, ); + return ( - - - } - {...popoverProps} - > - - -

- Press the down arrow key to interact with the calendar and select a - date. Press the escape button to close the calendar. -

-
+ + {locale => ( + + + } + {...popoverProps} + > + + +

+ {locale.datepicker.screenReaderMessageInput} +

+
+ )} +
); } } diff --git a/src/datepicker/locale.js b/src/datepicker/locale.js new file mode 100644 index 0000000000..38098a74be --- /dev/null +++ b/src/datepicker/locale.js @@ -0,0 +1,24 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type DatepickerLocaleT = {| + ariaLabel: string, + nextMonth: string, + previousMonth: string, + screenReaderMessageInput: string, +|}; + +const locale = { + ariaLabel: 'Select a date', + previousMonth: 'Previous month', + nextMonth: 'Next month', + screenReaderMessageInput: + 'Press the down arrow key to interact with the calendar and select a date. Press the escape button to close the calendar.', +}; + +export default locale; diff --git a/src/file-uploader/file-uploader.js b/src/file-uploader/file-uploader.js index ce250b0970..fbddbb959e 100644 --- a/src/file-uploader/file-uploader.js +++ b/src/file-uploader/file-uploader.js @@ -9,6 +9,7 @@ LICENSE file in the root directory of this source tree. import React from 'react'; import Dropzone from 'react-dropzone'; +import {LocaleContext} from '../locale/index.js'; import {Block} from '../block/index.js'; import {Button, KIND} from '../button/index.js'; import {getOverrides} from '../helpers/overrides.js'; @@ -78,126 +79,130 @@ function FileUploader(props: PropsT) { }; return ( - - - {!afterFileDrop && ( - - - Drop files here to upload - - - or - - - - - )} - - {afterFileDrop && ( - - {/** - * Below checks typeof value to ensure if progressAmount = 0 we will - * render the progress bar rather than the spinner. Providing a number - * value implies that we expect to have some progress percent in the - * future. We do not want to flash the spinner in this case. - */} - {typeof props.progressAmount === 'number' ? ( - ({ - backgroundColor: props.errorMessage - ? $theme.colors.negative - : $theme.colors.primary, - }), - }, - }} - /> - ) : ( - - - + + {locale => ( + + + {!afterFileDrop && ( + + + {locale.fileuploader.dropFilesToUpload} + + + {locale.fileuploader.or} + + + + )} - {(props.errorMessage || props.progressMessage) && - props.errorMessage ? ( - - {props.errorMessage} - - ) : ( - - {props.progressMessage} - - )} - {props.errorMessage ? ( - - ) : ( - + + {afterFileDrop && ( + + {/** + * Below checks typeof value to ensure if progressAmount = 0 we will + * render the progress bar rather than the spinner. Providing a number + * value implies that we expect to have some progress percent in the + * future. We do not want to flash the spinner in this case. + */} + {typeof props.progressAmount === 'number' ? ( + ({ + backgroundColor: props.errorMessage + ? $theme.colors.negative + : $theme.colors.primary, + }), + }, + }} + /> + ) : ( + + + + )} + {(props.errorMessage || props.progressMessage) && + props.errorMessage ? ( + + {props.errorMessage} + + ) : ( + + {props.progressMessage} + + )} + {props.errorMessage ? ( + + ) : ( + + )} + )} - - )} - - - - + + + +
+ )} + ); }} diff --git a/src/file-uploader/locale.js b/src/file-uploader/locale.js new file mode 100644 index 0000000000..d5e699673d --- /dev/null +++ b/src/file-uploader/locale.js @@ -0,0 +1,25 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type FileUploaderLocaleT = {| + dropFilesToUpload: string, + or: string, + browseFiles: string, + retry: string, + cancel: string, +|}; + +const locale = { + dropFilesToUpload: 'Drop files here to upload', + or: 'or', + browseFiles: 'Browse files', + retry: 'Retry Upload', + cancel: 'Cancel', +}; + +export default locale; diff --git a/src/index.js b/src/index.js index 9134a9603a..e1e0f8311c 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,8 @@ LICENSE file in the root directory of this source tree. */ // @flow export {styled, ThemeProvider} from './styles/index.js'; +import LocaleProvider from './locale/index.js'; +export {LocaleProvider}; export { createTheme, lightThemePrimitives, diff --git a/src/input/__tests__/__snapshots__/styled-components.test.js.snap b/src/input/__tests__/__snapshots__/styled-components.test.js.snap index 42e0d1f037..b123974b2e 100644 --- a/src/input/__tests__/__snapshots__/styled-components.test.js.snap +++ b/src/input/__tests__/__snapshots__/styled-components.test.js.snap @@ -250,6 +250,30 @@ Object { } `; +exports[`Input - StyledRoot - basic render: StyledRoot has correct default styles 1`] = ` +Object { + "color": "$theme.colors.foreground", + "display": "flex", + "fontFamily": "$theme.typography.font300.fontFamily", + "fontSize": "$theme.typography.font300.fontSize", + "fontWeight": "$theme.typography.font300.fontWeight", + "lineHeight": "$theme.typography.font300.lineHeight", + "width": "100%", +} +`; + +exports[`Input - StyledRoot - basic render: StyledRoot has correct styles when compact 1`] = ` +Object { + "color": "$theme.colors.foreground", + "display": "flex", + "fontFamily": "$theme.typography.font200.fontFamily", + "fontSize": "$theme.typography.font200.fontSize", + "fontWeight": "$theme.typography.font200.fontWeight", + "lineHeight": "$theme.typography.font200.lineHeight", + "width": "100%", +} +`; + exports[`Input - StyledStartEnhancer - basic render: StyledStartEnhancer has correct styles when compact and end position 1`] = ` Object { "backgroundColor": "$theme.colors.inputFillEnhancer", @@ -283,27 +307,3 @@ Object { "paddingTop": "$theme.sizing.scale400", } `; - -exports[`Input - StyledRoot - basic render: StyledRoot has correct default styles 1`] = ` -Object { - "color": "$theme.colors.foreground", - "display": "flex", - "fontFamily": "$theme.typography.font300.fontFamily", - "fontSize": "$theme.typography.font300.fontSize", - "fontWeight": "$theme.typography.font300.fontWeight", - "lineHeight": "$theme.typography.font300.lineHeight", - "width": "100%", -} -`; - -exports[`Input - StyledRoot - basic render: StyledRoot has correct styles when compact 1`] = ` -Object { - "color": "$theme.colors.foreground", - "display": "flex", - "fontFamily": "$theme.typography.font200.fontFamily", - "fontSize": "$theme.typography.font200.fontSize", - "fontWeight": "$theme.typography.font200.fontWeight", - "lineHeight": "$theme.typography.font200.lineHeight", - "width": "100%", -} -`; diff --git a/src/locale/en_US.js b/src/locale/en_US.js new file mode 100644 index 0000000000..bd0f80fdc8 --- /dev/null +++ b/src/locale/en_US.js @@ -0,0 +1,31 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +import accordion from '../accordion/locale.js'; +import breadcrumbs from '../breadcrumbs/locale.js'; +import datepicker from '../datepicker/locale.js'; +import buttongroup from '../button-group/locale.js'; +import fileuploader from '../file-uploader/locale.js'; +import modal from '../modal/locale.js'; +import pagination from '../pagination/locale.js'; +import select from '../select/locale.js'; +import toast from '../toast/locale.js'; + +const en_US = { + accordion, + breadcrumbs, + datepicker, + buttongroup, + fileuploader, + modal, + pagination, + select, + toast, +}; + +export default en_US; diff --git a/src/locale/index.js b/src/locale/index.js new file mode 100644 index 0000000000..6cf45050f8 --- /dev/null +++ b/src/locale/index.js @@ -0,0 +1,25 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow +import * as React from 'react'; +import extend from 'just-extend'; + +import type {LocaleT} from './types.js'; +import en_US from './en_US.js'; + +export const LocaleContext: React.Context = React.createContext(en_US); + +const LocaleProvider = (props: {locale: LocaleT, children: ?React.Node}) => { + const {locale, children} = props; + return ( + + {children} + + ); +}; + +export default LocaleProvider; diff --git a/src/locale/types.js b/src/locale/types.js new file mode 100644 index 0000000000..7e1a79ed27 --- /dev/null +++ b/src/locale/types.js @@ -0,0 +1,29 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +import type {AccordionLocaleT} from '../accordion/locale.js'; +import type {BreadcrumbLocaleT} from '../breadcrumbs/locale.js'; +import type {DatepickerLocaleT} from '../datepicker/locale.js'; +import type {ButtonGroupLocaleT} from '../button-group/locale.js'; +import type {FileUploaderLocaleT} from '../file-uploader/locale.js'; +import type {ModalLocaleT} from '../modal/locale.js'; +import type {PaginationLocaleT} from '../pagination/locale.js'; +import type {SelectLocaleT} from '../select/locale.js'; +import type {ToastLocaleT} from '../toast/locale.js'; + +export type LocaleT = {| + accordion: AccordionLocaleT, + breadcrumbs: BreadcrumbLocaleT, + datepicker: DatepickerLocaleT, + buttongroup: ButtonGroupLocaleT, + fileuploader: FileUploaderLocaleT, + modal: ModalLocaleT, + pagination: PaginationLocaleT, + select: SelectLocaleT, + toast: ToastLocaleT, +|}; diff --git a/src/modal/locale.js b/src/modal/locale.js new file mode 100644 index 0000000000..24729e6e16 --- /dev/null +++ b/src/modal/locale.js @@ -0,0 +1,17 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type ModalLocaleT = {| + close: string, +|}; + +const locale = { + close: 'Close', +}; + +export default locale; diff --git a/src/modal/modal.js b/src/modal/modal.js index 70f1db8b8f..18219a5421 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -9,6 +9,7 @@ LICENSE file in the root directory of this source tree. import * as React from 'react'; import ReactDOM from 'react-dom'; +import {LocaleContext} from '../locale/index.js'; import {getOverride, getOverrideProps} from '../helpers/overrides.js'; import {SIZE, ROLE, CLOSE_SOURCE} from './constants.js'; import {ownerDocument} from './utils.js'; @@ -275,47 +276,51 @@ class Modal extends React.Component { const children = this.getChildren(); return ( - - - - - - + {closeable ? ( + + + + ) : null} + {children} + + +
+ )} + ); } diff --git a/src/pagination/locale.js b/src/pagination/locale.js new file mode 100644 index 0000000000..116ccebf00 --- /dev/null +++ b/src/pagination/locale.js @@ -0,0 +1,21 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type PaginationLocaleT = {| + prev: string, + next: string, + preposition: string, +|}; + +const locale = { + prev: 'Prev', + next: 'Next', + preposition: 'of', +}; + +export default locale; diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index f41ce34330..8b738c61da 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -9,6 +9,7 @@ LICENSE file in the root directory of this source tree. import * as React from 'react'; import memoize from 'memoize-one'; // Files +import {LocaleContext} from '../locale/index.js'; import {Button, StyledBaseButton, KIND} from '../button/index.js'; import {StatefulMenu as Menu} from '../menu/index.js'; import { @@ -32,11 +33,7 @@ export default class Pagination extends React.PureComponent< PaginationStateT, > { static defaultProps = { - labels: { - prevButton: 'Prev', - nextButton: 'Next', - preposition: 'of', - }, + labels: {}, overrides: {}, }; @@ -131,69 +128,81 @@ export default class Pagination extends React.PureComponent< const options = this.getMenuOptions(numPages); return ( - - - - - {isMenuOpen && ( - // $FlowFixMe - + {locale => ( + + + + + {isMenuOpen && ( + // $FlowFixMe + + )} + + + {`${ + labels && labels.preposition + ? labels.preposition + : locale.pagination.preposition || '' + } ${numPages}`} + + - + {...nextButtonProps} + > + {labels && labels.nextButton + ? labels.nextButton + : locale.pagination.next} + + + )} + ); } } diff --git a/src/pagination/stateful-pagination.js b/src/pagination/stateful-pagination.js index 7e2600d99c..3b3a02baee 100644 --- a/src/pagination/stateful-pagination.js +++ b/src/pagination/stateful-pagination.js @@ -37,10 +37,5 @@ StatefulPagination.defaultProps = { currentPage: 1, }, stateReducer: (changeType: *, changes: *) => changes, - labels: { - prevButton: 'Prev', - nextButton: 'Next', - preposition: 'of', - }, overrides: {}, }; diff --git a/src/pagination/types.js b/src/pagination/types.js index 50cf0efa33..7d230b1c94 100644 --- a/src/pagination/types.js +++ b/src/pagination/types.js @@ -45,7 +45,7 @@ export type PaginationPropsT = CallbacksT & { /** The current page. */ currentPage: number, /** Set of labels to use for the buttons and preposition. */ - labels: LabelsT, + labels?: LabelsT, overrides?: OverridesT, }; @@ -57,7 +57,7 @@ export type StatefulPaginationPropsT = CallbacksT & { /** Max number of pages. */ numPages: number, /** Set of labels to use for the buttons and preposition. */ - labels: LabelsT, + labels?: LabelsT, /** Reducer function to manipulate internal state updates. */ stateReducer?: StateReducerFnT, /** Initial state populated into the component */ diff --git a/src/select/__tests__/__snapshots__/select.test.js.snap b/src/select/__tests__/__snapshots__/select.test.js.snap index 1d57f2b779..694f9c18e8 100644 --- a/src/select/__tests__/__snapshots__/select.test.js.snap +++ b/src/select/__tests__/__snapshots__/select.test.js.snap @@ -22,7 +22,6 @@ exports[`Select component renders component in search mode and false for multipl maxDropdownHeight="900px" multi={false} multiple={false} - noResultsMsg="No results found" onBlur={[MockFunction]} onBlurResetsInput={true} onChange={[MockFunction]} @@ -50,7 +49,6 @@ exports[`Select component renders component in search mode and false for multipl ] } overrides={Object {}} - placeholder="Select..." required={false} searchable={true} size="default" @@ -376,7 +374,6 @@ exports[`Select component renders component in search mode and true for multiple maxDropdownHeight="900px" multi={false} multiple={true} - noResultsMsg="No results found" onBlur={[MockFunction]} onBlurResetsInput={true} onChange={[MockFunction]} @@ -404,7 +401,6 @@ exports[`Select component renders component in search mode and true for multiple ] } overrides={Object {}} - placeholder="Select..." required={false} searchable={true} size="default" @@ -730,7 +726,6 @@ exports[`Select component renders component in select mode and false for multipl maxDropdownHeight="900px" multi={false} multiple={false} - noResultsMsg="No results found" onBlur={[MockFunction]} onBlurResetsInput={true} onChange={[MockFunction]} @@ -758,7 +753,6 @@ exports[`Select component renders component in select mode and false for multipl ] } overrides={Object {}} - placeholder="Select..." required={false} searchable={true} size="default" @@ -1083,7 +1077,6 @@ exports[`Select component renders component in select mode and true for multiple maxDropdownHeight="900px" multi={false} multiple={true} - noResultsMsg="No results found" onBlur={[MockFunction]} onBlurResetsInput={true} onChange={[MockFunction]} @@ -1111,7 +1104,6 @@ exports[`Select component renders component in select mode and true for multiple ] } overrides={Object {}} - placeholder="Select..." required={false} searchable={true} size="default" diff --git a/src/select/default-props.js b/src/select/default-props.js index 2219b0c7de..48e9825178 100644 --- a/src/select/default-props.js +++ b/src/select/default-props.js @@ -28,7 +28,6 @@ const defaultProps = { labelKey: 'label', maxDropdownHeight: '900px', multi: false, - noResultsMsg: 'No results found', onBlur: () => {}, onBlurResetsInput: true, onChange: () => {}, @@ -41,7 +40,6 @@ const defaultProps = { openOnClick: true, options: [], overrides: {}, - placeholder: 'Select...', required: false, searchable: true, size: SIZE.default, diff --git a/src/select/locale.js b/src/select/locale.js new file mode 100644 index 0000000000..98982d6442 --- /dev/null +++ b/src/select/locale.js @@ -0,0 +1,19 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type SelectLocaleT = {| + noResultsMsg: string, + placeholder: string, +|}; + +const locale = { + noResultsMsg: 'No results found', + placeholder: 'Select...', +}; + +export default locale; diff --git a/src/select/select.js b/src/select/select.js index 92d2c42f86..f58bae876a 100644 --- a/src/select/select.js +++ b/src/select/select.js @@ -36,6 +36,7 @@ import { Search as SearchIconComponent, } from '../icon/index.js'; import defaultProps from './default-props.js'; +import {LocaleContext} from '../locale/index.js'; import type { PropsT, @@ -44,6 +45,7 @@ import type { OptionT, ChangeActionT, } from './types.js'; +import type {LocaleT} from '../locale/types.js'; class Select extends React.Component { static defaultProps = defaultProps; @@ -547,6 +549,7 @@ class Select extends React.Component { renderValue( valueArray: ValueT, isOpen: boolean, + locale: LocaleT, ): ?React.Node | Array { const {overrides = {}} = this.props; const sharedProps = this.getSharedProps(); @@ -563,7 +566,7 @@ class Select extends React.Component { ); return showPlaceholder ? ( - {this.props.placeholder} + {this.props.placeholder || locale.select.placeholder} ) : null; } @@ -746,7 +749,7 @@ class Select extends React.Component { } } - renderMenu(options: ValueT, valueArray: ValueT) { + renderMenu(options: ValueT, valueArray: ValueT, locale: LocaleT) { const { error, getOptionLabel, @@ -784,7 +787,7 @@ class Select extends React.Component { } else if (noResultsMsg) { const noResults = { [valueKey]: 'NO_RESULTS_FOUND', - [labelKey]: noResultsMsg, + [labelKey]: noResultsMsg || locale.select.noResultsMsg, disabled: true, }; return ; @@ -854,29 +857,37 @@ class Select extends React.Component { } sharedProps.$isOpen = isOpen; return ( - (this.wrapper = ref)} {...sharedProps} {...rootProps}> - - {type === TYPE.search ? this.renderSearch() : null} - - {this.renderValue(valueArray, isOpen)} - {this.renderInput()} - - - {this.renderLoading()} - {this.renderClear()} - {type === TYPE.select ? this.renderArrow() : null} - - - {isOpen ? this.renderMenu(options, valueArray) : null} - + + {locale => ( + (this.wrapper = ref)} + {...sharedProps} + {...rootProps} + > + + {type === TYPE.search ? this.renderSearch() : null} + + {this.renderValue(valueArray, isOpen, locale)} + {this.renderInput()} + + + {this.renderLoading()} + {this.renderClear()} + {type === TYPE.select ? this.renderArrow() : null} + + + {isOpen ? this.renderMenu(options, valueArray, locale) : null} + + )} + ); } } diff --git a/src/select/types.js b/src/select/types.js index e852e8c713..692ab55c52 100644 --- a/src/select/types.js +++ b/src/select/types.js @@ -112,7 +112,7 @@ export type PropsT = { /** Defines if multiple options can be selected. */ multi: boolean, /** Message to be displayed if no options is found for a search query. */ - noResultsMsg: React.Node, + noResultsMsg?: React.Node, onBlur: (e: Event) => void, /** Defines if the input value is reset to an empty string when a blur event happens on the select. */ onBlurResetsInput: boolean, @@ -135,7 +135,7 @@ export type PropsT = { options: ?ValueT, overrides: OverridesT, /** Sets the placeholder. */ - placeholder: React.Node, + placeholder?: React.Node, /** Defines if the select field is required to have a selection. */ required: boolean, /** Defines if the search functionality id enabled. */ diff --git a/src/toast/locale.js b/src/toast/locale.js new file mode 100644 index 0000000000..54b74a469f --- /dev/null +++ b/src/toast/locale.js @@ -0,0 +1,17 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +// @flow + +export type ToastLocaleT = {| + close: string, +|}; + +const locale = { + close: 'Close', +}; + +export default locale; diff --git a/src/toast/toast.js b/src/toast/toast.js index 5f8a9dfd55..5a3b60b7f4 100644 --- a/src/toast/toast.js +++ b/src/toast/toast.js @@ -13,6 +13,7 @@ import { CloseIconSvg as StyledCloseIcon, } from './styled-components.js'; import {KIND, TYPE} from './constants.js'; +import {LocaleContext} from '../locale/index.js'; import type { ToastPropsT, @@ -155,30 +156,34 @@ class Toast extends React.Component { return null; } return ( - - {closeable ? ( - + {locale => ( + - ) : null} - {typeof children === 'function' - ? children({dismiss: this.dismiss}) - : children} - + {...bodyProps} + // the properties below have to go after overrides + onBlur={this.onBlur} + onFocus={this.onFocus} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + {closeable ? ( + + ) : null} + {typeof children === 'function' + ? children({dismiss: this.dismiss}) + : children} + + )} + ); } } diff --git a/yarn.lock b/yarn.lock index 8bba64b1e0..eb3d2ae556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9158,6 +9158,11 @@ junk@^1.0.1: resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592" integrity sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI= +just-extend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" + integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== + kefir@^3.7.3: version "3.8.3" resolved "https://registry.yarnpkg.com/kefir/-/kefir-3.8.3.tgz#8e0ab10084ed8a01cbb5d4f7f18a0b859f7b9bd9"