feat!: implement i18n in Paragon components and in docs site (#1100)

Adds support for i18n translations in Paragon components, including the commands to push/pull translations from Transifex in an automated process. This change itself does not guarantee the components are truly translated yet as they may still need a translator and/or a reviewer to approve the translation in Transifex before a translation is pulled.

To upgrade, ensure you are using at least `react-intl@5.25.0` in your application.

BREAKING CHANGE: By adding i18n support to the Paragon design system, we are introducing a peer dependency on `react-intl@5.25.0` or greater. This may be a breaking change for some consumers, if your repository:
* Uses v1 of `@edx/frontend-platform`
* Uses older version of `react-intl` than v5.25.0 directly.
* Does not use `react-intl`.
This commit is contained in:
Viktor Rusakov 2022-06-17 13:59:59 +03:00 committed by GitHub
parent aecdf1f211
commit 53e0ac632b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2636 additions and 1558 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ npm-debug.log
coverage
jest*
dist
src/i18n/transifex_input.json
# gatsby files
www/.cache/

8
.tx/config Normal file
View File

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[edx-platform.paragon]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = STRUCTURED_JSON

View File

@ -7,3 +7,46 @@ build:
rm -rf dist/__mocks__
rm -rf dist/setupTest.js
node build-scss.js
transifex_langs = "ar,fr,es_419,zh_CN"
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
NPM_TESTS=build i18n_extract lint test
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
.PHONY: test.npm.*
test.npm.%: validate-no-uncommitted-package-lock-changes
test -d node_modules || $(MAKE) requirements
npm run $(*)
.PHONY: requirements
requirements: ## install ci requirements
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
npm run-script i18n_extract
extract_translations: | requirements i18n.extract
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json

134
README.md
View File

@ -167,6 +167,140 @@ JSX code blocks in the markdown file can be made interactive with the live attri
Visit the documentation at [http://localhost:8000](http://localhost:8000) and navigate to see your README.md powered page and workbench. Changes to the README.md file will auto refresh the page.
### Internationalization
Paragon supports internationalization for its components out of the box with the support of [react-intl](https://formatjs.io/docs/react-intl/). You can view translated strings for each component on the docs site after switching language on a settings tab.
#### For consumers
Since we are using ``react-intl`` that means that your whole app needs to be wrapped in its context, e.g.
```javascript
import { IntlProvider } from 'react-intl';
import { messages as paragonMessages } from '@edx/paragon';
ReactDOM.render(
<IntlProvider locale={usersLocale} messages={paragonMessages[usersLocale]}>
<App />
</IntlProvider>,
document.getElementById('root')
)
```
Note that if you are using ``@edx/frontend-platform``'s ``AppProvider`` component you don't need a separate context,
you would only need to add Paragon's messages like this
```javascript
import { APP_READY, subscribe, initialize } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { messages as paragonMessages } from '@edx/paragon';
import App from './App';
// this is your app's i18n messages
import appMessages from './i18n';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
<App />
</AppProvider>,
document.getElementById('root')
)
})
initialize({
// this will add your app's messages as well as Paragon's messages to your app
messages: [
appMessages,
paragonMessages,
],
// here you will typically provide other configurations for you app
...
});
```
#### For developers
When developing a new component you should generally follow three rules:
1. The component should not have **any** hardcoded strings as it would be impossible for consumers to translate it
2. Internationalize all default values of props that expect strings, i.e.
- For places where you need to display a string, and it's okay if it is a React element use ``FormattedMessage``, e.g. (see [Alert](src/Alert/index.jsx) component for a full example)
```javascript
import { FormattedMessage } from 'react-intl';
<FormattedMessage
id="pgn.Alert.closeLabel"
defaultMessage="Dismiss"
description="Label of a close button on Alert component"
/>
```
- For places where the display string has to be a plain JavaScript string use ``formatMessage``, this would require access to ``intl`` object from ``react-intl``, e.g.
- For class components use ``injectIntl`` HOC
```javascript
import { injectIntl } from 'react-intl';
class MyClassComponent extends React.Component {
render() {
const { altText, intl } = this.props;
const intlAltText = altText || intl.formatMessage({
id: 'pgn.MyComponent.altText',
defaultMessage: 'Close',
description: 'Close label for Toast component',
});
return (
<IconButton
alt={intlCloseLabel}
onClick={() => {}}
variant="primary"
/>
)
}
}
export default injectIntl(MyClassComponent);
```
- For functional components use ``useIntl`` hook
```javascript
import { useIntl } from 'react-intl';
const MyFunctionComponent = ({ altText }) => {
const intls = useIntl();
const intlAltText = altText || intl.formatMessage({
id: 'pgn.MyComponent.altText',
defaultMessage: 'Close',
description: 'Close label for Toast component',
});
return (
<IconButton
alt={intlCloseLabel}
onClick={() => {}}
variant="primary"
/>
)
export default MyFunctionComponent;
```
**Notes on the format above**:
- `id` is required and must be a dot-separated string of the format `pgn.<componentName>.<subcomponentName>.<propName>`
- The `defaultMessage` is required, and should be the English display string.
- The `description` is optional, but highly recommended, this text gives context to translators about the string.
3. If your component expects a string as a prop, allow the prop to also be an element since consumers may want to also pass instance of their own translated string, for example you might define a string prop like this:
```javascript
MyComponent.PropTypes = {
myProp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
```
### Developing locally against MFE
If you want to test the changes with local MFE setup, you need to create a "module.config.js" file in your MFE's directory containing local module overrides. After that the webpack build for your application will automatically pick your local version of Paragon and use it. The example of module.config.js file looks like this (for more details about module.config.js, refer to the [frontend-build documentation](https://github.com/edx/frontend-build#local-module-configuration-for-webpack).):

View File

@ -56,10 +56,10 @@ Consequences
ReactDOM.render(
<AppProvider>
<App />
</IntlProvider>,
</AppProvider>,
document.getElementById('root')
)
)
})
initialize({
// this will add your app's messages as well as Paragon's messages to your app

2002
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"react": "^16.14.0",
"react-dom": "^16.14.0",
"@edx/brand-openedx": "^1.1.0",
"@edx/frontend-platform": "^1.15.6",
"@edx/frontend-platform": "^2.0.0",
"core-js": "^3.22.2",
"regenerator-runtime": "^0.13.9"
},

986
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,8 @@
"test:watch": "npm run test -- --watch",
"generate-component": "cd component-generator && npm start",
"generate-component-lint": "cd component-generator && npm run lint",
"generate-changelog": "node generate-changelog.js"
"generate-changelog": "node generate-changelog.js",
"i18n_extract": "formatjs extract 'src/**/*.jsx' --out-file ./src/i18n/transifex_input.json --format transifex"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
@ -64,7 +65,8 @@
},
"peerDependencies": {
"react": "^16.8.6 || ^17.0.0",
"react-dom": "^16.8.6 || ^17.0.0"
"react-dom": "^16.8.6 || ^17.0.0",
"react-intl": "^5.25.0"
},
"devDependencies": {
"@babel/cli": "^7.16.8",
@ -74,6 +76,7 @@
"@babel/preset-env": "^7.16.8",
"@babel/preset-react": "^7.16.7",
"@edx/eslint-config": "^3.0.0",
"@formatjs/cli": "^4.8.4",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.1",
"@testing-library/jest-dom": "^5.16.3",

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import renderer, { act } from 'react-test-renderer';
import { Context as ResponsiveContext } from 'react-responsive';
import breakpoints from '../utils/breakpoints';
@ -7,56 +8,65 @@ import Button from '../Button';
import Alert from './index';
import { Info } from '../../icons';
// eslint-disable-next-line react/prop-types
const AlertWrapper = ({ children, ...props }) => (
<IntlProvider locale="en" messages={{}}>
<Alert {...props}>
{children}
</Alert>
</IntlProvider>
);
describe('<Alert />', () => {
it('renders without any props', () => {
const tree = renderer.create((
<Alert>Alert</Alert>
<AlertWrapper>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders with icon prop', () => {
const tree = renderer.create((
<Alert icon={Info}>Alert</Alert>
<AlertWrapper icon={Info}>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders with dismissible prop', () => {
const tree = renderer.create((
<Alert dismissible>Alert</Alert>
<AlertWrapper dismissible>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('handles dismissible onClose', () => {
const mockOnClose = jest.fn();
const wrapper = mount((
<Alert onClose={mockOnClose} dismissible>Alert</Alert>
<AlertWrapper onClose={mockOnClose} dismissible>Alert</AlertWrapper>
));
wrapper.find('.btn').simulate('click');
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('renders with button prop', () => {
const tree = renderer.create((
<Alert actions={[<Button>Hello</Button>]}>Alert</Alert>
<AlertWrapper actions={[<Button>Hello</Button>]}>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('handles button onClick', () => {
const mockOnClick = jest.fn();
const wrapper = mount((
<Alert actions={[<Button onClick={mockOnClick}>Hello</Button>]}>Alert</Alert>
<AlertWrapper actions={[<Button onClick={mockOnClick}>Hello</Button>]}>Alert</AlertWrapper>
));
wrapper.find('.btn').simulate('click');
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('renders with button and dismissible props', () => {
const tree = renderer.create((
<Alert actions={[<Button>Hello</Button>]} dismissible>Alert</Alert>
<AlertWrapper actions={[<Button>Hello</Button>]} dismissible>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders with stacked prop', () => {
const tree = renderer.create((
<Alert stacked actions={[<Button>Hello</Button>]} dismissible>Alert</Alert>
<AlertWrapper stacked actions={[<Button>Hello</Button>]} dismissible>Alert</AlertWrapper>
)).toJSON();
expect(tree).toMatchSnapshot();
});
@ -65,7 +75,7 @@ describe('<Alert />', () => {
act(() => {
tree = renderer.create((
<ResponsiveContext.Provider value={{ width: breakpoints.extraSmall.maxWidth }}>
<Alert dismissible>Alert</Alert>
<AlertWrapper dismissible>Alert</AlertWrapper>
</ResponsiveContext.Provider>
)).toJSON();
});

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import BaseAlert from 'react-bootstrap/Alert';
import divWithClassName from 'react-bootstrap/divWithClassName';
import { FormattedMessage } from 'react-intl';
import { useMediaQuery } from 'react-responsive';
import { Icon } from '..';
import breakpoints from '../utils/breakpoints';
@ -66,7 +67,13 @@ const Alert = React.forwardRef(({
variant="tertiary"
onClick={onClose}
>
{closeLabel}
{closeLabel || (
<FormattedMessage
id="pgn.Alert.closeLabel"
defaultMessage="Dismiss"
description="Label of a close button on Alert component"
/>
)}
</Button>
)}
{actions && actions.map(cloneActionElement)}
@ -122,8 +129,8 @@ Alert.propTypes = {
actions: PropTypes.arrayOf(PropTypes.element),
/** Position of the dismiss and call-to-action buttons. Defaults to ``false``. */
stacked: PropTypes.bool,
/** Sets the text for alert close button. */
closeLabel: PropTypes.string,
/** Sets the text for alert close button, defaults to 'Dismiss'. */
closeLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
Alert.defaultProps = {
@ -133,7 +140,7 @@ Alert.defaultProps = {
actions: undefined,
dismissible: false,
onClose: () => {},
closeLabel: ALERT_CLOSE_LABEL_TEXT,
closeLabel: undefined,
show: true,
stacked: false,
};

View File

@ -6,6 +6,7 @@ import SidebarFilters from './SidebarFilters';
import DataTableContext from './DataTableContext';
const DataTableLayout = ({
filtersTitle,
className,
children,
}) => {
@ -15,7 +16,7 @@ const DataTableLayout = ({
<div className={classNames('pgn__data-table-layout-wrapper', className)}>
{(showFiltersInSidebar && setFilter) && (
<div className="pgn__data-table-layout-sidebar">
<SidebarFilters />
<SidebarFilters title={filtersTitle} />
</div>
)}
<div className="pgn__data-table-layout-main">
@ -27,11 +28,13 @@ const DataTableLayout = ({
DataTableLayout.defaultProps = {
className: null,
filtersTitle: undefined,
};
DataTableLayout.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
filtersTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
export default DataTableLayout;

View File

@ -1,12 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Button } from '..';
const ExpandAll = ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }) => (
<span {...getToggleAllRowsExpandedProps()}>
{isAllRowsExpanded
? <Button variant="link" size="inline">Collapse all</Button>
: <Button variant="link" size="inline">Expand all</Button>}
{isAllRowsExpanded ? (
<Button variant="link" size="inline">
<FormattedMessage
id="pgn.DataTable.ExpandAll.collapseAllLabel"
defaultMessage="Collapse all"
description="Label of an action button that collapses all expandable rows of DataTable."
/>
</Button>
) : (
<Button variant="link" size="inline">
<FormattedMessage
id="pgn.DataTable.ExpandAll.expandAllLabel"
defaultMessage="Expand all"
description="Label of an action button that expands all expandable rows of DataTable."
/>
</Button>
)}
</span>
);

View File

@ -1,5 +1,6 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Button } from '..';
import DataTableContext from './DataTableContext';
@ -23,7 +24,15 @@ const FilterStatus = ({
size={size}
onClick={() => setAllFilters([])}
>
{clearFiltersText}
{clearFiltersText === undefined
? (
<FormattedMessage
id="pgn.DataTable.FilterStatus.clearFiltersText"
defaultMessage="Clear filters"
description="A text that appears on the `Clear filters` button"
/>
)
: clearFiltersText}
</Button>
</div>
);
@ -38,8 +47,8 @@ FilterStatus.defaultProps = {
variant: 'link',
/** The size of the `FilterStatus`. */
size: 'inline',
/** A text that appears on the `Clear filters` button. */
clearFiltersText: 'Clear Filters',
/** A text that appears on the `Clear filters` button, defaults to 'Clear filters'. */
clearFiltersText: undefined,
/** Whether to display applied filters. */
showFilteredFields: true,
};
@ -49,7 +58,7 @@ FilterStatus.propTypes = {
buttonClassName: PropTypes.string,
variant: PropTypes.string,
size: PropTypes.string,
clearFiltersText: PropTypes.string,
clearFiltersText: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
showFilteredFields: PropTypes.bool,
};

View File

@ -1,24 +1,39 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import DataTableContext from './DataTableContext';
const RowStatus = ({ className }) => {
const RowStatus = ({ className, statusText }) => {
const { page, rows, itemCount } = useContext(DataTableContext);
const pageSize = page?.length || rows?.length;
if (!pageSize) {
return null;
}
return (<div className={className}>Showing {pageSize} of {itemCount}.</div>);
return (
<div className={className}>
{statusText || (
<FormattedMessage
id="pgn.DataTable.RowStatus.statusText"
defaultMessage="Showing {pageSize} of {itemCount}."
description="A text describing how many rows is shown in the table"
values={{ itemCount, pageSize }}
/>
)}
</div>
);
};
RowStatus.propTypes = {
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** A text describing how many rows is shown in the table. */
statusText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
RowStatus.defaultProps = {
className: undefined,
statusText: undefined,
};
export default RowStatus;

View File

@ -1,15 +1,25 @@
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import DataTableContext from './DataTableContext';
import FilterStatus from './FilterStatus';
const SidebarFilters = () => {
const SidebarFilters = ({ title }) => {
const { state, columns } = useContext(DataTableContext);
const availableFilters = useMemo(() => columns.filter((column) => column.canFilter), [columns]);
const filtersApplied = state?.filters && state.filters.length > 0;
return (
<div className="pgn__data-table-side-filters">
<h3 className="pgn__data-table-side-filters-title">Filters</h3>
<h3 className="pgn__data-table-side-filters-title">
{title || (
<FormattedMessage
id="pgn.DataTable.SidebarFilters.title"
defaultMessage="Filters"
description="Title for the sidebar filters component"
/>
)}
</h3>
<hr />
{availableFilters.map(column => (
<div
@ -32,4 +42,13 @@ const SidebarFilters = () => {
);
};
SidebarFilters.propTypes = {
/** Specifies the title to show near the filters, default to 'Filters'. */
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
SidebarFilters.defaultProps = {
title: undefined,
};
export default SidebarFilters;

View File

@ -1,5 +1,6 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Button } from '../..';
import DataTableContext from '../DataTableContext';
@ -14,12 +15,31 @@ const BaseSelectionStatus = ({
numSelectedRows,
onSelectAll,
onClear,
selectAllText,
allSelectedText,
selectedText,
}) => {
const { itemCount } = useContext(DataTableContext);
const isAllRowsSelected = numSelectedRows === itemCount;
const intlAllSelectedText = allSelectedText || (
<FormattedMessage
id="pgn.DataTable.BaseSelectionStatus.allSelectedText"
defaultMessage="All {numSelectedRows} selected"
description="Text for all selected label"
values={{ numSelectedRows }}
/>
);
const intlSelectedText = selectedText || (
<FormattedMessage
id="pgn.DataTable.BaseSelectionStatus.selectedText"
defaultMessage="{numSelectedRows} selected"
description="Text for selected label"
values={{ numSelectedRows }}
/>
);
return (
<div className={className}>
<span>{isAllRowsSelected && 'All '}{numSelectedRows} selected </span>
<span>{isAllRowsSelected ? intlAllSelectedText : intlSelectedText}</span>
{!isAllRowsSelected && (
<Button
className={SELECT_ALL_TEST_ID}
@ -27,7 +47,14 @@ const BaseSelectionStatus = ({
size="inline"
onClick={onSelectAll}
>
Select all {itemCount}
{selectAllText || (
<FormattedMessage
id="pgn.DataTable.BaseSelectionStatus.selectAllText"
defaultMessage="Select all {itemCount}"
description="A label for select all button."
values={{ itemCount }}
/>
)}
</Button>
)}
{numSelectedRows > 0 && (
@ -37,7 +64,13 @@ const BaseSelectionStatus = ({
size="inline"
onClick={onClear}
>
{clearSelectionText}
{clearSelectionText || (
<FormattedMessage
id="pgn.DataTable.BaseSelectionStatus.clearSelectionText"
defaultMessage="Clear selection"
description="A label of clear all selection button."
/>
)}
</Button>
)}
</div>
@ -46,14 +79,29 @@ const BaseSelectionStatus = ({
BaseSelectionStatus.defaultProps = {
className: undefined,
selectAllText: undefined,
allSelectedText: undefined,
selectedText: undefined,
clearSelectionText: undefined,
};
BaseSelectionStatus.propTypes = {
/** A class name to append to the base element */
className: PropTypes.string,
clearSelectionText: PropTypes.string.isRequired,
/** A text that appears on the `Clear selection` button, defaults to 'Clear selection' */
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
/** Count of selected rows in the table. */
numSelectedRows: PropTypes.number.isRequired,
/** A handler for 'Select all' button. */
onSelectAll: PropTypes.func.isRequired,
/** A handler for 'Clear selection' button. */
onClear: PropTypes.func.isRequired,
/** A text that appears on the `Select all` button, defaults to 'Select All {itemCount}' */
selectAllText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
/** A text that appears when all items have been selected, defaults to 'All {numSelectedRows} selected' */
allSelectedText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
/** A text that appears when some items have been selected, defaults to '{numSelectedRows} selected' */
selectedText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
export default BaseSelectionStatus;

View File

@ -13,7 +13,6 @@ import {
getUnselectedPageRows,
getRowIds,
} from './data/helpers';
import { CLEAR_SELECTION_TEXT } from './data/constants';
const ControlledSelectionStatus = ({ className, clearSelectionText }) => {
const {
@ -49,12 +48,14 @@ const ControlledSelectionStatus = ({ className, clearSelectionText }) => {
ControlledSelectionStatus.defaultProps = {
className: undefined,
clearSelectionText: CLEAR_SELECTION_TEXT,
clearSelectionText: undefined,
};
ControlledSelectionStatus.propTypes = {
/** A class name to append to the base element */
className: PropTypes.string,
clearSelectionText: PropTypes.string,
/** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
export default ControlledSelectionStatus;

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import DataTableContext from '../DataTableContext';
import BaseSelectionStatus from './BaseSelectionStatus';
import { CLEAR_SELECTION_TEXT } from './data/constants';
const SelectionStatus = ({ className, clearSelectionText }) => {
const { toggleAllRowsSelected, selectedFlatRows } = useContext(DataTableContext);
@ -21,13 +20,13 @@ const SelectionStatus = ({ className, clearSelectionText }) => {
SelectionStatus.propTypes = {
/** A class name to append to the base element */
className: PropTypes.string,
/** A text that appears on the `Clear selection` button */
clearSelectionText: PropTypes.string,
/** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};
SelectionStatus.defaultProps = {
className: undefined,
clearSelectionText: CLEAR_SELECTION_TEXT,
clearSelectionText: undefined,
};
export default SelectionStatus;

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import ControlledSelectionStatus from '../ControlledSelectionStatus';
import { clearSelectionAction, setSelectAllRowsAllPagesAction, setSelectedRowsAction } from '../data/actions';
@ -24,7 +25,11 @@ const instance = {
// eslint-disable-next-line react/prop-types
const ControlledSelectionStatusWrapper = ({ value, props = {} }) => (
<DataTableContext.Provider value={value}><ControlledSelectionStatus {...props} /></DataTableContext.Provider>
<IntlProvider locale="en" messages={{}}>
<DataTableContext.Provider value={value}>
<ControlledSelectionStatus {...props} />
</DataTableContext.Provider>
</IntlProvider>
);
describe('<ControlledSelectionStatus />', () => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import SelectionStatus from '../SelectionStatus';
import DataTableContext from '../../DataTableContext';
@ -17,7 +18,11 @@ const instance = {
// eslint-disable-next-line react/prop-types
const SelectionStatusWrapper = ({ value, props = {} }) => (
<DataTableContext.Provider value={value}><SelectionStatus {...props} /></DataTableContext.Provider>
<IntlProvider locale="en" messages={{}}>
<DataTableContext.Provider value={value}>
<SelectionStatus {...props} />
</DataTableContext.Provider>
</IntlProvider>
);
describe('<SelectionStatus />', () => {

View File

@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import * as reactTable from 'react-table';
import { IntlProvider } from 'react-intl';
import DataTable from '..';
import TableControlBar from '../TableControlBar';
@ -97,43 +98,52 @@ const DataTableContextProviderChild = ({ children }) => {
);
};
// eslint-disable-next-line react/prop-types
const DataTableWrapper = ({ children, ...tableProps }) => (
<IntlProvider locale="en" messages={{}}>
<DataTable {...tableProps}>
{children}
</DataTable>
</IntlProvider>
);
describe('<DataTable />', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('displays the empty table component if empty', () => {
const wrapper = mount(<DataTable {...props} data={[]} />);
const wrapper = mount(<DataTableWrapper {...props} data={[]} />);
expect(wrapper.find(EmptyTable).length).toEqual(1);
});
it('accepts an empty table component', () => {
const wrapper = mount(<DataTable {...props} data={[]} EmptyTableComponent={EmptyTest} />);
const wrapper = mount(<DataTableWrapper {...props} data={[]} EmptyTableComponent={EmptyTest} />);
expect(wrapper.find(EmptyTable).length).toEqual(0);
expect(wrapper.find(EmptyTest).length).toEqual(1);
});
it('displays a control bar', () => {
const wrapper = mount(<DataTable {...props} />);
const wrapper = mount(<DataTableWrapper {...props} />);
const controlBar = wrapper.find(TableControlBar);
expect(controlBar.length).toEqual(1);
expect(controlBar.text()).toEqual('Showing 7 of 7.');
});
it('displays a table', () => {
const wrapper = mount(<DataTable {...props} />);
const wrapper = mount(<DataTableWrapper {...props} />);
const table = wrapper.find(Table);
expect(table.length).toEqual(1);
expect(table.find('th').length).toEqual(3);
expect(table.find('tr').length).toEqual(8);
});
it('displays a table footer', () => {
const wrapper = mount(<DataTable {...props} />);
const wrapper = mount(<DataTableWrapper {...props} />);
expect(wrapper.find(TableFooter).length).toEqual(1);
});
it('adds a column when table is selectable', () => {
const wrapper = mount(<DataTable {...props} isSelectable />);
const wrapper = mount(<DataTableWrapper {...props} isSelectable />);
const tableHeaders = wrapper.find(Table).find('th');
expect(tableHeaders.length).toEqual(props.columns.length + 1);
});
it('adds additional columns', () => {
const wrapper = mount(<DataTable {...props} additionalColumns={additionalColumns} />);
const wrapper = mount(<DataTableWrapper {...props} additionalColumns={additionalColumns} />);
const tableHeaders = wrapper.find(Table).find('th');
expect(tableHeaders.length).toEqual(props.columns.length + additionalColumns.length);
expect(wrapper.text()).toContain(additionalColumns[0].Header);
@ -141,7 +151,7 @@ describe('<DataTable />', () => {
});
test('calls useTable with the data and columns', () => {
const spy = jest.spyOn(reactTable, 'useTable');
mount(<DataTable {...props} />);
mount(<DataTableWrapper {...props} />);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].columns).toEqual(props.columns);
expect(spy.mock.calls[0][0].data).toEqual(props.data);
@ -157,7 +167,7 @@ describe('<DataTable />', () => {
[{ manualSortBy: true, manualFilters: true, manualPagination: true, pageCount: 1 }, { manualFilters: true, manualPagination: true, manualSortBy: true }],
])('calls useTable with the correct manual settings %#', (additionalProps, expected) => {
const spy = jest.spyOn(reactTable, 'useTable');
mount(<DataTable {...props} {...additionalProps} />);
mount(<DataTableWrapper {...props} {...additionalProps} />);
expect(spy.mock.calls[0][0].manualFilters).toEqual(expected.manualFilters);
expect(spy.mock.calls[0][0].manualPagination).toEqual(expected.manualPagination);
expect(spy.mock.calls[0][0].manualSortBy).toEqual(expected.manualSortBy);
@ -165,11 +175,11 @@ describe('<DataTable />', () => {
it('passes the initial state to useTable', () => {
const spy = jest.spyOn(reactTable, 'useTable');
const initialState = { foo: 'bar' };
mount(<DataTable {...props} initialState={initialState} />);
mount(<DataTableWrapper {...props} initialState={initialState} />);
expect(spy.mock.calls[0][0].initialState).toEqual(initialState);
});
it('displays loading state', () => {
const wrapper = mount(<DataTable {...props} isLoading />);
const wrapper = mount(<DataTableWrapper {...props} isLoading />);
const tableContainer = wrapper.find('.pgn__data-table-container');
const spinner = wrapper.find('.pgn__data-table-spinner');
expect(tableContainer.hasClass('is-loading')).toEqual(true);
@ -182,9 +192,9 @@ describe('<DataTable />', () => {
describe('controlled table selections', () => {
it('passes initial controlledTableSelections to context', () => {
const wrapper = mount(
<DataTable {...props}>
<DataTableWrapper {...props}>
<DataTableContextProviderChild />
</DataTable>,
</DataTableWrapper>,
);
const contextValue = wrapper.find('div.context-value').prop('data-contextvalue');
const { controlledTableSelections } = contextValue;
@ -195,7 +205,7 @@ describe('<DataTable />', () => {
});
it('passes appropriate selection props to context with active selections', () => {
const wrapper = mount(
<DataTable {...props}><DataTableContextProviderChild /></DataTable>,
<DataTableWrapper {...props}><DataTableContextProviderChild /></DataTableWrapper>,
);
// verify there are no current selections

View File

@ -1,10 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import ExpandAll from '../ExpandAll';
const ExpandAllWrapper = (props) => (
<IntlProvider locale="en" messages={{}}>
<ExpandAll {...props} />
</IntlProvider>
);
describe('<ExpandAll />', () => {
it('renders expand all element if not all rows are expanded', () => {
const wrapper = mount(<ExpandAll getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded={false} />);
const wrapper = mount(<ExpandAllWrapper getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded={false} />);
const labelWrapper = wrapper.find('span');
expect(labelWrapper.exists()).toEqual(true);
const collapseButton = wrapper.find('button');
@ -12,7 +19,7 @@ describe('<ExpandAll />', () => {
expect(collapseButton.text()).toContain('Expand all');
});
it('renders collapse all element if all rows are expanded', () => {
const wrapper = mount(<ExpandAll getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded />);
const wrapper = mount(<ExpandAllWrapper getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded />);
const labelWrapper = wrapper.find('span');
expect(labelWrapper.exists()).toEqual(true);
const collapseButton = wrapper.find('button');

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import FilterStatus from '../FilterStatus';
import { Button } from '../..';
@ -17,7 +18,6 @@ const filterProps = {
className: 'filterClass',
showFilteredFields: true,
};
const filterPropsNoFiltered = {
...filterProps,
showFilteredFields: false,
@ -26,7 +26,11 @@ const filterPropsNoFiltered = {
// eslint-disable-next-line react/prop-types
const FilterStatusWrapper = ({ value, props }) => (
<DataTableContext.Provider value={value}><FilterStatus {...props} /></DataTableContext.Provider>
<IntlProvider locale="en" messages={{}}>
<DataTableContext.Provider value={value}>
<FilterStatus {...props} />
</DataTableContext.Provider>
</IntlProvider>
);
describe('<FilterStatus />', () => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import RowStatus from '../RowStatus';
import DataTableContext from '../DataTableContext';
@ -14,7 +15,12 @@ const statusProps = {
// eslint-disable-next-line react/prop-types
const RowStatusWrapper = ({ value = instance, props = statusProps }) => (
<DataTableContext.Provider value={value}><RowStatus {...props} /></DataTableContext.Provider>);
<IntlProvider locale="en" messages={{}}>
<DataTableContext.Provider value={value}>
<RowStatus {...props} />
</DataTableContext.Provider>
</IntlProvider>
);
describe('<RowStatus />', () => {
it('returns null if there is no pageSize', () => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import SmartStatus from '../SmartStatus';
import DataTableContext from '../DataTableContext';
@ -25,7 +26,12 @@ const instance = {
// eslint-disable-next-line react/prop-types
const SmartStatusWrapper = ({ value, props }) => (
<DataTableContext.Provider value={value}><SmartStatus {...props} /></DataTableContext.Provider>);
<IntlProvider locale="en" messages={{}}>
<DataTableContext.Provider value={value}>
<SmartStatus {...props} />
</DataTableContext.Provider>
</IntlProvider>
);
describe('<SmartStatus />', () => {
it('Shows the selection status if rows are selected', () => {

View File

@ -1,7 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { IntlProvider } from 'react-intl';
import Toast from './index';
/* eslint-disable-next-line react/prop-types */
const ToastWrapper = ({ children, ...props }) => (
<IntlProvider>
<Toast {...props}>
{children}
</Toast>
</IntlProvider>
);
describe('<Toast />', () => {
const onCloseHandler = () => {};
const props = {
@ -10,7 +20,7 @@ describe('<Toast />', () => {
};
it('renders optional action as link', () => {
const wrapper = mount((
<Toast
<ToastWrapper
{...props}
action={{
label: 'Optional action',
@ -18,13 +28,13 @@ describe('<Toast />', () => {
}}
>
Success message.
</Toast>));
</ToastWrapper>));
const toastLink = wrapper.find('a.btn');
expect(toastLink).toHaveLength(1);
});
it('renders optional action as button', () => {
const wrapper = mount((
<Toast
<ToastWrapper
{...props}
action={{
label: 'Optional action',
@ -32,17 +42,17 @@ describe('<Toast />', () => {
}}
>
Success message.
</Toast>));
</ToastWrapper>));
const toastButton = wrapper.find('button.btn');
expect(toastButton).toHaveLength(1);
});
it('autohide is set to false on onMouseOver and true on onMouseLeave', () => {
const wrapper = mount((
<Toast
<ToastWrapper
{...props}
>
Success message.
</Toast>));
</ToastWrapper>));
wrapper.prop('onMouseOver');
setTimeout(() => {
const toast = wrapper.find(Toast);
@ -58,11 +68,11 @@ describe('<Toast />', () => {
});
it('autohide is set to false onFocus and true onBlur', () => {
const wrapper = mount((
<Toast
<ToastWrapper
{...props}
>
Success message.
</Toast>));
</ToastWrapper>));
wrapper.prop('onFocus');
setTimeout(() => {
const toast = wrapper.find(Toast);

View File

@ -3,6 +3,8 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import BaseToast from 'react-bootstrap/Toast';
import { useIntl } from 'react-intl';
import ToastContainer from './ToastContainer';
import { Button, IconButton, Icon } from '..';
import { Close } from '../../icons';
@ -13,7 +15,13 @@ export const TOAST_DELAY = 5000;
function Toast({
action, children, className, closeLabel, onClose, show, ...rest
}) {
const intl = useIntl();
const [autoHide, setAutoHide] = useState(true);
const intlCloseLabel = closeLabel || intl.formatMessage({
id: 'pgn.Toast.closeLabel',
defaultMessage: 'Close',
description: 'Close label for Toast component',
});
return (
<ToastContainer>
<BaseToast
@ -34,7 +42,7 @@ function Toast({
<div className="toast-header-btn-container">
<IconButton
iconAs={Icon}
alt={closeLabel}
alt={intlCloseLabel}
className="align-self-start"
src={Close}
onClick={onClose}
@ -61,7 +69,7 @@ function Toast({
Toast.defaultProps = {
action: null,
closeLabel: TOAST_CLOSE_LABEL_TEXT,
closeLabel: undefined,
delay: TOAST_DELAY,
className: undefined,
};
@ -89,8 +97,7 @@ Toast.propTypes = {
onClick: PropTypes.func,
}),
/**
* Alt text for the `Toast`'s dismiss button. The recommended use is an i18n value.
* The default is an English string.
* Alt text for the `Toast`'s dismiss button. Defaults to 'Close'.
*/
closeLabel: PropTypes.string,
/** Time in milliseconds for which the `Toast` will display. */

31
src/i18n/index.js Normal file
View File

@ -0,0 +1,31 @@
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_KR.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_BR.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
import zhcnMessages from './messages/zh_CN.json';
const messages = {
ar: arMessages,
ca: caMessages,
'es-419': es419Messages,
fr: frMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
'zh-cn': zhcnMessages,
};
export default messages;

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -186,3 +186,5 @@ export {
useFlexLayout,
} from 'react-table';
export { default as Bubble } from './Bubble';
export { default as messages } from './i18n';

View File

@ -1,2 +1,3 @@
FEATURE_LANGUAGE_SWITCHER=true
SEGMENT_KEY=''
FEATURE_ENABLE_AXE='true'

View File

@ -28,6 +28,7 @@ exports.onCreateWebpackConfig = ({ actions }) => {
// in ./node_modules
react: path.resolve(__dirname, 'node_modules/react/'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom/'),
'react-intl': path.resolve(__dirname, 'node_modules/react-intl/'),
},
},
})

432
www/package-lock.json generated
View File

@ -30,11 +30,11 @@
"prism-react-renderer": "^1.2.1",
"prop-types": "^15.8.1",
"react": "^16.8.6",
"react-copy-to-clipboard": "^5.1.0",
"react-docgen": "^5.4.0",
"react-dom": "^16.8.6",
"react-focus-on": "^3.5.4",
"react-helmet": "^6.1.0",
"react-intl": "^5.25.0",
"react-live": "^2.4.0",
"rehype-autolink-headings": "^5.1.0",
"rehype-slug": "^4.0.1",
@ -4761,6 +4761,132 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
"dependencies": {
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/fast-memoize": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/fast-memoize/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/icu-skeleton-parser": "1.3.6",
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/intl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz",
"integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.1.0",
"@formatjs/intl-displaynames": "5.4.3",
"@formatjs/intl-listformat": "6.5.3",
"intl-messageformat": "9.13.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"typescript": "^4.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-displaynames": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz",
"integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/intl-displaynames/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/intl-listformat": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz",
"integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/intl-listformat/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.2.25",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@formatjs/intl-localematcher/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@formatjs/intl/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "0.2.36",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
@ -5819,6 +5945,15 @@
"@types/unist": "*"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
@ -8999,14 +9134,6 @@
"node": ">=0.10.0"
}
},
"node_modules/copy-to-clipboard": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js": {
"version": "3.20.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz",
@ -15944,6 +16071,14 @@
"node": "*"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -16264,6 +16399,22 @@
"node": ">= 0.4"
}
},
"node_modules/intl-messageformat": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.1.0",
"tslib": "^2.1.0"
}
},
"node_modules/intl-messageformat/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -21429,18 +21580,6 @@
"react": "^15.3.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"dependencies": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^15.3.0 || 16 || 17 || 18"
}
},
"node_modules/react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
@ -21792,6 +21931,37 @@
"react": ">=16.3.0"
}
},
"node_modules/react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
"integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/icu-messageformat-parser": "2.1.0",
"@formatjs/intl": "2.2.1",
"@formatjs/intl-displaynames": "5.4.3",
"@formatjs/intl-listformat": "6.5.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "16 || 17 || 18",
"hoist-non-react-statics": "^3.3.2",
"intl-messageformat": "9.13.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "^16.3.0 || 17 || 18",
"typescript": "^4.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/react-intl/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -25007,11 +25177,6 @@
"node": ">=8.0"
}
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -30021,6 +30186,140 @@
}
}
},
"@formatjs/ecma402-abstract": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
"requires": {
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/fast-memoize": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/icu-messageformat-parser": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/icu-skeleton-parser": "1.3.6",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/icu-skeleton-parser": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/intl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz",
"integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.1.0",
"@formatjs/intl-displaynames": "5.4.3",
"@formatjs/intl-listformat": "6.5.3",
"intl-messageformat": "9.13.0",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/intl-displaynames": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz",
"integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/intl-listformat": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz",
"integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/intl-localematcher": "0.2.25",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@formatjs/intl-localematcher": {
"version": "0.2.25",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.36",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
@ -30873,6 +31172,15 @@
"@types/unist": "*"
}
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
@ -33380,14 +33688,6 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
"copy-to-clipboard": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
"requires": {
"toggle-selection": "^1.0.6"
}
},
"core-js": {
"version": "3.20.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz",
@ -38657,6 +38957,14 @@
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -38886,6 +39194,24 @@
"side-channel": "^1.0.4"
}
},
"intl-messageformat": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.1.0",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -42577,15 +42903,6 @@
"@babel/runtime": "^7.12.13"
}
},
"react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"requires": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
}
},
"react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
@ -42851,6 +43168,30 @@
"react-side-effect": "^2.1.0"
}
},
"react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
"integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==",
"requires": {
"@formatjs/ecma402-abstract": "1.11.4",
"@formatjs/icu-messageformat-parser": "2.1.0",
"@formatjs/intl": "2.2.1",
"@formatjs/intl-displaynames": "5.4.3",
"@formatjs/intl-listformat": "6.5.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "16 || 17 || 18",
"hoist-non-react-statics": "^3.3.2",
"intl-messageformat": "9.13.0",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -45311,11 +45652,6 @@
"is-number": "^7.0.0"
}
},
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@ -3,8 +3,8 @@
"description": "Paragon Pattern Library Documentation",
"version": "1.0.0",
"dependencies": {
"@edx/brand-edx.org": "^2.0.6",
"@docsearch/react": "^3.0.0",
"@edx/brand-edx.org": "^2.0.6",
"@edx/brand-openedx": "^1.1.0",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@mdx-js/mdx": "^1.6.22",
@ -29,6 +29,7 @@
"react-focus-on": "^3.5.4",
"react-helmet": "^6.1.0",
"react-live": "^2.4.0",
"react-intl": "^5.25.0",
"rehype-autolink-headings": "^5.1.0",
"rehype-slug": "^4.0.1",
"sass": "^1.32.13",

View File

@ -10,6 +10,7 @@ import theme from 'prism-react-renderer/themes/duotoneDark';
import {
LiveProvider, LiveEditor, LiveError, LivePreview,
} from 'react-live';
import { FormattedMessage, useIntl } from 'react-intl';
import * as ParagonReact from '~paragon-react'; // eslint-disable-line
import * as ParagonIcons from '~paragon-icons'; // eslint-disable-line
import MiyazakiCard from './exampleComponents/MiyazakiCard';
@ -41,7 +42,12 @@ CollapsibleLiveEditor.propTypes = {
children: PropTypes.node.isRequired,
};
function CodeBlock({ children, className, live }) {
function CodeBlock({
children,
className,
live,
}) {
const intl = useIntl();
const language = className ? className.replace(/language-/, '') : 'jsx';
if (live) {
@ -58,6 +64,8 @@ function CodeBlock({ children, className, live }) {
useMemo,
MiyazakiCard,
HipsterIpsum,
FormattedMessage,
formatMessage: intl.formatMessage,
MenuIcon: ParagonIcons.Menu,
}}
theme={theme}

View File

@ -4,6 +4,8 @@ import { MDXRenderer } from 'gatsby-plugin-mdx';
import PropType from './PropType';
import { Badge, Card } from '~paragon-react'; // eslint-disable-line
const IGNORED_COMPONENT_PROPS = ['intl'];
const DefaultValue = ({ value }) => {
if (!value || value === 'undefined') { return null; }
return (
@ -69,7 +71,9 @@ const PropsTable = ({ props: componentProps, displayName, content }) => (
{content && <div className="small mb-3">{content}</div>}
{componentProps.length > 0 ? (
<ul className="list-unstyled">
{componentProps.map(metadata => <Prop key={metadata.name} {...metadata} />)}
{componentProps
.filter(metadata => !IGNORED_COMPONENT_PROPS.includes(metadata.name))
.map(metadata => <Prop key={metadata.name} {...metadata} />)}
</ul>
) : <div className="pb-3 pl-4">This component does not receive any props.</div>}
</Card>

View File

@ -4,20 +4,20 @@ import {
Form,
Icon,
IconButton,
Stack,
} from '~paragon-react';
import { Close } from '~paragon-icons';
import { FEATURES, LANGUAGES } from '../config';
import SettingsContext from '../context/SettingsContext';
import { THEMES } from '../../theme-config';
const Settings = () => {
const {
theme: currentTheme,
onThemeChange,
settings,
handleSettingsChange,
showSettings,
closeSettings,
direction,
onDirectionChange,
} = useContext(SettingsContext);
return (
@ -38,12 +38,12 @@ const Settings = () => {
size="sm"
/>
</div>
<div className="pgn__settings-form-wrapper">
<Stack gap={3}>
<Form.Group>
<Form.Control
as="select"
value={currentTheme}
onChange={onThemeChange}
value={settings.theme}
onChange={(e) => handleSettingsChange('theme', e.target.value)}
floatingLabel="Theme"
>
{THEMES.map(theme => (
@ -59,15 +59,34 @@ const Settings = () => {
<Form.Group>
<Form.Control
as="select"
value={direction}
onChange={onDirectionChange}
value={settings.direction}
onChange={(e) => handleSettingsChange('direction', e.target.value)}
floatingLabel="Direction"
>
<option value="ltr">Left to right</option>
<option value="rtl">Right to left</option>
</Form.Control>
</Form.Group>
</div>
{FEATURES.LANGUAGE_SWITCHER && (
<Form.Group>
<Form.Control
as="select"
value={settings.language}
onChange={(e) => handleSettingsChange('language', e.target.value)}
floatingLabel="Component Language"
>
{LANGUAGES.map(lang => (
<option
key={lang.code}
value={lang.code}
>
{lang.label}
</option>
))}
</Form.Control>
</Form.Group>
)}
</Stack>
</Sheet>
);
};

View File

@ -1,6 +1,6 @@
// import hasFeatureFlagEnabled from './utils/hasFeatureFlagEnabled';
const hasFeatureFlagEnabled = require('./utils/hasFeatureFlagEnabled');
// export const EXAMPLE_FEATURE = 'EXAMPLE_FEATURE';
const FEATURE_LANGUAGE_SWITCHER = 'LANGUAGE_SWITCHER';
// Feature flags used throughout the site should be configured here.
// You should generally allow two ways of enabling a feature flag:
@ -11,12 +11,70 @@
// See DIRECTION_SWITCHER feature for example of configuring feature flags this way.
// 2. As a query parameter in the URL, using hasFeatureFlagEnabled util function.
// This will allow to enable feature flag by providing its name as a feature?
// query parameter in the URL. (e.g. to enable DIRECTION_SWITCHER feature you would append
// '?feature=DIRECTION_SWITCHER' to the URL)
// query parameter in the URL. (e.g. to enable LANGUAGE_SWITCHER feature you would append
// '?feature=LANGUAGE_SWITCHER' to the URL)
const FEATURES = {
LANGUAGE_SWITCHER: process.env.FEATURE_LANGUAGE_SWITCHER || hasFeatureFlagEnabled(FEATURE_LANGUAGE_SWITCHER),
};
// export const FEATURES = {
// EXAMPLE_FEATURE: process.env.EXAMPLE_FEATURE || hasFeatureFlagEnabled(EXAMPLE_FEATURE),
// };
const LANGUAGES = [
{
label: 'English',
code: 'en',
},
{
label: 'Arabic',
code: 'ar',
},
{
label: 'Catalan',
code: 'ca',
},
{
label: 'Chinese',
code: 'zh-cn',
},
{
label: 'French',
code: 'fr',
},
{
label: 'Hebrew',
code: 'he',
},
{
label: 'Indonesian',
code: 'id',
},
{
label: 'Polish',
code: 'pl',
},
{
label: 'Russian',
code: 'ru',
},
{
label: 'Thai',
code: 'th',
},
{
label: 'Ukrainian',
code: 'uk',
},
{
label: 'Spanish',
code: 'es-419',
},
{
label: 'Korean',
code: 'ko-kr',
},
{
label: 'Portuguese',
code: 'pt-br',
},
];
const INSIGHTS_TABS = Object.freeze({
SUMMARY: 'Summary',
@ -42,4 +100,7 @@ const INSIGHTS_PAGES = [
module.exports = {
INSIGHTS_TABS,
INSIGHTS_PAGES,
FEATURES,
LANGUAGES,
FEATURE_LANGUAGE_SWITCHER,
};

View File

@ -1,13 +1,14 @@
import React, { createContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { IntlProvider } from 'react-intl';
import { messages } from '~paragon-react';
import { THEMES } from '../../theme-config';
const defaultValue = {
theme: 'openedx-theme',
onThemeChange: () => {},
direction: 'ltr',
onDirectionChange: () => {},
settings: {},
handleSettingsChange: () => {},
};
export const SettingsContext = createContext(defaultValue);
@ -15,38 +16,33 @@ export const SettingsContext = createContext(defaultValue);
const SettingsContextProvider = ({ children }) => {
// gatsby does not have access to the localStorage during the build (and first render)
// so sadly we cannot initialize theme with value from localStorage
const [theme, setTheme] = useState('openedx-theme');
const [settings, setSettings] = useState({
theme: 'openedx-theme',
direction: 'ltr',
language: 'en',
});
const [showSettings, setShowSettings] = useState(false);
const [direction, setDirection] = useState('ltr');
const handleDirectionChange = (e) => {
document.body.setAttribute('dir', e.target.value);
setDirection(e.target.value);
global.localStorage.setItem('pgn__direction', e.target.value);
global.analytics.track('Direction change', { direction: e.target.value });
const handleSettingsChange = (key, value) => {
if (key === 'direction') {
document.body.setAttribute('dir', value);
}
setSettings(prevState => ({ ...prevState, [key]: value }));
global.localStorage.setItem('pgn__settings', JSON.stringify({ ...settings, [key]: value }));
global.analytics.track(`${key[0].toUpperCase() + key.slice(1)} change`, { [key]: value });
};
const handleThemeChange = (e) => {
setTheme(e.target.value);
global.localStorage.setItem('pgn__theme', e.target.value);
global.analytics.track('Theme change', { theme: e.target.value });
};
const handleSettingsChange = (value) => {
const toggleSettings = (value) => {
setShowSettings(value);
global.analytics.track('Toggle Settings', { value: value ? 'show' : 'hide' });
};
// this hook will be called after the first render, so we can safely access localStorage
useEffect(() => {
const savedTheme = global.localStorage.getItem('pgn__theme');
if (savedTheme) {
setTheme(savedTheme);
}
const savedDirection = global.localStorage.getItem('pgn__direction');
if (savedDirection) {
document.body.setAttribute('dir', savedDirection);
setDirection(savedDirection);
const savedSettings = JSON.parse(global.localStorage.getItem('pgn__settings'));
if (savedSettings) {
setSettings(savedSettings);
document.body.setAttribute('dir', savedSettings.direction);
}
if (!global.analytics) {
global.analytics = {};
@ -55,13 +51,11 @@ const SettingsContextProvider = ({ children }) => {
}, []);
const contextValue = {
theme,
direction,
settings,
showSettings,
onThemeChange: handleThemeChange,
onDirectionChange: handleDirectionChange,
closeSettings: () => handleSettingsChange(false),
openSettings: () => handleSettingsChange(true),
handleSettingsChange,
closeSettings: () => toggleSettings(false),
openSettings: () => toggleSettings(true),
};
return (
@ -77,12 +71,14 @@ const SettingsContextProvider = ({ children }) => {
<link
key={themeInfo.stylesheet}
href={`/static/${themeInfo.stylesheet}.css`}
rel={`stylesheet${theme === themeInfo.stylesheet ? '' : ' alternate'}`}
rel={`stylesheet${settings.theme === themeInfo.stylesheet ? '' : ' alternate'}`}
type="text/css"
/>
))}
</Helmet>
{children}
<IntlProvider messages={messages[settings.language]} locale={settings.language.split('-')[0]}>
{children}
</IntlProvider>
</SettingsContext.Provider>
);
};

View File

@ -4,7 +4,7 @@
* @param {string} featureFlag
* @returns true if feature flag is in `?feature` query parameter
*/
export default function hasFeatureFlagEnabled(featureFlag) {
function hasFeatureFlagEnabled(featureFlag) {
const { location } = global;
if (!location) {
return false;
@ -13,3 +13,5 @@ export default function hasFeatureFlagEnabled(featureFlag) {
const features = searchParams.getAll('feature');
return features.includes(featureFlag);
}
module.exports = hasFeatureFlagEnabled;