From 0d29c25f25aa22eed33b4e2752c21f0d7575354d Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Fri, 18 Feb 2022 11:20:13 +0200 Subject: [PATCH 01/10] feat: add i18n to docs site and to DataTable component --- package.json | 1 + src/Alert/index.jsx | 15 ++- src/DataTable/DataTableLayout.jsx | 5 +- src/DataTable/ExpandAll.jsx | 21 +++- src/DataTable/FilterStatus.jsx | 15 ++- src/DataTable/RowStatus.jsx | 19 +++- src/DataTable/SidebarFilters.jsx | 23 ++++- .../selection/BaseSelectionStatus.jsx | 56 ++++++++++- .../selection/ControlledSelectionStatus.jsx | 5 +- src/DataTable/selection/SelectionStatus.jsx | 7 +- src/Toast/index.jsx | 19 ++-- src/i18n/index.js | 31 ++++++ src/i18n/messages/ar.json | 3 + src/i18n/messages/ca.json | 3 + src/i18n/messages/es_419.json | 3 + src/i18n/messages/fr.json | 3 + src/i18n/messages/he.json | 3 + src/i18n/messages/id.json | 3 + src/i18n/messages/ko_KR.json | 3 + src/i18n/messages/pl.json | 3 + src/i18n/messages/pt_BR.json | 3 + src/i18n/messages/ru.json | 3 + src/i18n/messages/th.json | 3 + src/i18n/messages/uk.json | 3 + src/i18n/messages/zh_CN.json | 3 + src/index.js | 2 + www/.env.development | 1 + www/package-lock.json | 46 +++++++++ www/package.json | 1 + www/src/components/CodeBlock.jsx | 13 ++- www/src/components/PropsTable.jsx | 2 +- www/src/components/Settings.jsx | 28 +++++- www/src/config.js | 68 ++++++++++++- www/src/context/SettingsContext.jsx | 96 ++++++++++++------- 34 files changed, 436 insertions(+), 77 deletions(-) create mode 100644 src/i18n/index.js create mode 100644 src/i18n/messages/ar.json create mode 100644 src/i18n/messages/ca.json create mode 100644 src/i18n/messages/es_419.json create mode 100644 src/i18n/messages/fr.json create mode 100644 src/i18n/messages/he.json create mode 100644 src/i18n/messages/id.json create mode 100644 src/i18n/messages/ko_KR.json create mode 100644 src/i18n/messages/pl.json create mode 100644 src/i18n/messages/pt_BR.json create mode 100644 src/i18n/messages/ru.json create mode 100644 src/i18n/messages/th.json create mode 100644 src/i18n/messages/uk.json create mode 100644 src/i18n/messages/zh_CN.json diff --git a/package.json b/package.json index bafc3805d6..bdf3ab9b84 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "prop-types": "^15.8.1", "react-bootstrap": "^1.6.4", "react-focus-on": "^3.5.4", + "react-intl": "2.9.0", "react-popper": "^2.2.5", "react-proptype-conditional-require": "^1.0.4", "react-responsive": "^8.2.0", diff --git a/src/Alert/index.jsx b/src/Alert/index.jsx index 6759ae5fa2..5f37ad4cfb 100644 --- a/src/Alert/index.jsx +++ b/src/Alert/index.jsx @@ -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 || ( + + )} )} {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, }; diff --git a/src/DataTable/DataTableLayout.jsx b/src/DataTable/DataTableLayout.jsx index c74ce96d89..d17cad40f9 100644 --- a/src/DataTable/DataTableLayout.jsx +++ b/src/DataTable/DataTableLayout.jsx @@ -6,6 +6,7 @@ import SidebarFilters from './SidebarFilters'; import DataTableContext from './DataTableContext'; const DataTableLayout = ({ + filtersTitle, className, children, }) => { @@ -15,7 +16,7 @@ const DataTableLayout = ({
{(showFiltersInSidebar && setFilter) && (
- +
)}
@@ -27,11 +28,13 @@ const DataTableLayout = ({ DataTableLayout.defaultProps = { className: null, + filtersTitle: undefined, }; DataTableLayout.propTypes = { className: PropTypes.string, children: PropTypes.node.isRequired, + filtersTitle: PropTypes.string, }; export default DataTableLayout; diff --git a/src/DataTable/ExpandAll.jsx b/src/DataTable/ExpandAll.jsx index 0e79df1aa5..ddbf9c9da4 100644 --- a/src/DataTable/ExpandAll.jsx +++ b/src/DataTable/ExpandAll.jsx @@ -1,12 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; import { Button } from '..'; const ExpandAll = ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }) => ( {isAllRowsExpanded - ? - : } + ? ( + + ) + : ( + + )} ); diff --git a/src/DataTable/FilterStatus.jsx b/src/DataTable/FilterStatus.jsx index fe62521ff2..7e7b5399b1 100644 --- a/src/DataTable/FilterStatus.jsx +++ b/src/DataTable/FilterStatus.jsx @@ -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,13 @@ const FilterStatus = ({ size={size} onClick={() => setAllFilters([])} > - {clearFiltersText} + {clearFiltersText || ( + + )}
); @@ -38,8 +45,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 +56,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, }; diff --git a/src/DataTable/RowStatus.jsx b/src/DataTable/RowStatus.jsx index 67e98cd681..6976ed155a 100644 --- a/src/DataTable/RowStatus.jsx +++ b/src/DataTable/RowStatus.jsx @@ -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 (
Showing {pageSize} of {itemCount}.
); + return ( +
+ {statusText || ( + + )} +
+ ); }; 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; diff --git a/src/DataTable/SidebarFilters.jsx b/src/DataTable/SidebarFilters.jsx index b673e104d5..056a4afdff 100644 --- a/src/DataTable/SidebarFilters.jsx +++ b/src/DataTable/SidebarFilters.jsx @@ -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 (
-

Filters

+

+ {title || ( + + )} +


{availableFilters.map(column => (
{ ); }; +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; diff --git a/src/DataTable/selection/BaseSelectionStatus.jsx b/src/DataTable/selection/BaseSelectionStatus.jsx index e049c02f2d..1269aa7a93 100644 --- a/src/DataTable/selection/BaseSelectionStatus.jsx +++ b/src/DataTable/selection/BaseSelectionStatus.jsx @@ -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 || ( + + ); + const intlSelectedText = selectedText || ( + + ); return (
- {isAllRowsSelected && 'All '}{numSelectedRows} selected + {isAllRowsSelected ? intlAllSelectedText : intlSelectedText} {!isAllRowsSelected && ( )} {numSelectedRows > 0 && ( @@ -37,7 +64,13 @@ const BaseSelectionStatus = ({ size="inline" onClick={onClear} > - {clearSelectionText} + {clearSelectionText || ( + + )} )}
@@ -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; diff --git a/src/DataTable/selection/ControlledSelectionStatus.jsx b/src/DataTable/selection/ControlledSelectionStatus.jsx index b894327176..7cc04eaa5a 100644 --- a/src/DataTable/selection/ControlledSelectionStatus.jsx +++ b/src/DataTable/selection/ControlledSelectionStatus.jsx @@ -13,7 +13,6 @@ import { getUnselectedPageRows, getRowIds, } from './data/helpers'; -import { CLEAR_SELECTION_TEXT } from './data/constants'; const ControlledSelectionStatus = ({ className, clearSelectionText }) => { const { @@ -49,11 +48,13 @@ 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, + /** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */ clearSelectionText: PropTypes.string, }; diff --git a/src/DataTable/selection/SelectionStatus.jsx b/src/DataTable/selection/SelectionStatus.jsx index 9ce2f4696e..0398cabe5e 100644 --- a/src/DataTable/selection/SelectionStatus.jsx +++ b/src/DataTable/selection/SelectionStatus.jsx @@ -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; diff --git a/src/Toast/index.jsx b/src/Toast/index.jsx index 935fb5eff5..f2ccf8e7e8 100644 --- a/src/Toast/index.jsx +++ b/src/Toast/index.jsx @@ -3,6 +3,8 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import BaseToast from 'react-bootstrap/Toast'; +import { injectIntl } from 'react-intl'; + import ToastContainer from './ToastContainer'; import { Button, IconButton, Icon } from '..'; import { Close } from '../../icons'; @@ -11,9 +13,14 @@ export const TOAST_CLOSE_LABEL_TEXT = 'Close'; export const TOAST_DELAY = 5000; function Toast({ - action, children, className, closeLabel, onClose, show, ...rest + action, children, className, closeLabel, onClose, show, intl, ...rest }) { const [autoHide, setAutoHide] = useState(true); + const intlCloseLabel = closeLabel || intl.formatMessage({ + id: 'pgn.Toast.closeLabel', + defaultMessage: 'Close', + description: 'Close label for Toast component', + }); return ( ( {content &&
{content}
} {componentProps.length > 0 ? (
    - {componentProps.map(metadata => )} + {componentProps.map(metadata => metadata.name !== 'intl' && )}
) :
This component does not receive any props.
} diff --git a/www/src/components/Settings.jsx b/www/src/components/Settings.jsx index 95077eb6ff..37066eb6ee 100644 --- a/www/src/components/Settings.jsx +++ b/www/src/components/Settings.jsx @@ -7,13 +7,14 @@ import { } 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, @@ -42,8 +43,8 @@ const Settings = () => { handleSettingsChange('theme', e.target.value)} floatingLabel="Theme" > {THEMES.map(theme => ( @@ -67,6 +68,25 @@ const Settings = () => { + {FEATURES.LANGUAGE_SWITCHER && ( + + handleSettingsChange('language', e.target.value)} + floatingLabel="Component Language" + > + {LANGUAGES.map(lang => ( + + ))} + + + )}
); diff --git a/www/src/config.js b/www/src/config.js index 2d64d39a51..6ea56c3bc8 100644 --- a/www/src/config.js +++ b/www/src/config.js @@ -1,6 +1,6 @@ -// import hasFeatureFlagEnabled from './utils/hasFeatureFlagEnabled'; +import hasFeatureFlagEnabled from './utils/hasFeatureFlagEnabled'; -// export const EXAMPLE_FEATURE = 'EXAMPLE_FEATURE'; +export 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: @@ -13,10 +13,68 @@ // 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) +export 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), -// }; +export 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', diff --git a/www/src/context/SettingsContext.jsx b/www/src/context/SettingsContext.jsx index bdc23ca016..ace638ac21 100644 --- a/www/src/context/SettingsContext.jsx +++ b/www/src/context/SettingsContext.jsx @@ -1,13 +1,46 @@ import React, { createContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; +import { messages } from '~paragon-react'; + +import { IntlProvider, addLocaleData } from 'react-intl'; +import arLocale from 'react-intl/locale-data/ar'; +import enLocale from 'react-intl/locale-data/en'; +import esLocale from 'react-intl/locale-data/es'; +import frLocale from 'react-intl/locale-data/fr'; +import zhLocale from 'react-intl/locale-data/zh'; +import caLocale from 'react-intl/locale-data/ca'; +import heLocale from 'react-intl/locale-data/he'; +import idLocale from 'react-intl/locale-data/id'; +import koLocale from 'react-intl/locale-data/ko'; +import plLocale from 'react-intl/locale-data/pl'; +import ptLocale from 'react-intl/locale-data/pt'; +import ruLocale from 'react-intl/locale-data/ru'; +import thLocale from 'react-intl/locale-data/th'; +import ukLocale from 'react-intl/locale-data/uk'; + import { THEMES } from '../../theme-config'; +addLocaleData([ + ...arLocale, + ...caLocale, + ...heLocale, + ...idLocale, + ...plLocale, + ...ruLocale, + ...thLocale, + ...ukLocale, + ...enLocale, + ...esLocale, + ...frLocale, + ...zhLocale, + ...koLocale, + ...ptLocale, +]); + const defaultValue = { - theme: 'openedx-theme', - onThemeChange: () => {}, - direction: 'ltr', - onDirectionChange: () => {}, + settings: {}, + handleSettingsChange: () => {}, }; export const SettingsContext = createContext(defaultValue); @@ -15,38 +48,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 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 = (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 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 +83,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 +103,14 @@ const SettingsContextProvider = ({ children }) => { ))} - {children} + + {children} + ); }; From b766533a8431201a07b4eb1b61157bc7c5145adb Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Tue, 15 Mar 2022 11:13:00 +0200 Subject: [PATCH 02/10] chore: update snapshots and fix proptypes --- package-lock.json | 46 +++++++++++++++++++ src/Alert/__snapshots__/Alert.test.jsx.snap | 12 +++-- src/DataTable/DataTableLayout.jsx | 2 +- src/DataTable/ExpandAll.jsx | 36 +++++++-------- src/DataTable/FilterStatus.jsx | 2 +- src/DataTable/RowStatus.jsx | 2 +- .../selection/ControlledSelectionStatus.jsx | 2 +- src/DataTable/tests/FilterStatus.test.jsx | 10 ---- src/Toast/Toast.test.jsx | 26 +++++++---- src/Toast/index.jsx | 4 +- www/src/components/Settings.jsx | 5 +- 11 files changed, 100 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf5c0485fc..d07dd0bd51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30386,6 +30386,14 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "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" + } + }, "hook-std": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", @@ -30686,6 +30694,32 @@ "side-channel": "^1.0.4" } }, + "intl-format-cache": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-2.2.9.tgz", + "integrity": "sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==" + }, + "intl-messageformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", + "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", + "requires": { + "intl-messageformat-parser": "1.4.0" + } + }, + "intl-messageformat-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", + "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=" + }, + "intl-relativeformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz", + "integrity": "sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw==", + "requires": { + "intl-messageformat": "^2.0.0" + } + }, "into-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", @@ -37833,6 +37867,18 @@ "use-sidecar": "^1.1.2" } }, + "react-intl": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", + "integrity": "sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==", + "requires": { + "hoist-non-react-statics": "^3.3.0", + "intl-format-cache": "^2.0.5", + "intl-messageformat": "^2.1.0", + "intl-relativeformat": "^2.1.0", + "invariant": "^2.1.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/src/Alert/__snapshots__/Alert.test.jsx.snap b/src/Alert/__snapshots__/Alert.test.jsx.snap index 577697ee78..40d6111bfb 100644 --- a/src/Alert/__snapshots__/Alert.test.jsx.snap +++ b/src/Alert/__snapshots__/Alert.test.jsx.snap @@ -25,7 +25,9 @@ exports[` renders with button and dismissible props 1`] = ` onClick={[Function]} type="button" > - Dismiss + + Dismiss +
@@ -163,7 +167,9 @@ exports[` renders with stacked prop 1`] = ` onClick={[Function]} type="button" > - Dismiss + + Dismiss + - ) - : ( - - )} + {isAllRowsExpanded ? ( + + ) : ( + + )} ); diff --git a/src/DataTable/FilterStatus.jsx b/src/DataTable/FilterStatus.jsx index 7e7b5399b1..f4f922f3b3 100644 --- a/src/DataTable/FilterStatus.jsx +++ b/src/DataTable/FilterStatus.jsx @@ -27,7 +27,7 @@ const FilterStatus = ({ {clearFiltersText || ( )} diff --git a/src/DataTable/RowStatus.jsx b/src/DataTable/RowStatus.jsx index 6976ed155a..bac4210a61 100644 --- a/src/DataTable/RowStatus.jsx +++ b/src/DataTable/RowStatus.jsx @@ -15,7 +15,7 @@ const RowStatus = ({ className, statusText }) => { {statusText || ( diff --git a/src/DataTable/selection/ControlledSelectionStatus.jsx b/src/DataTable/selection/ControlledSelectionStatus.jsx index 7cc04eaa5a..aa7b21fe8f 100644 --- a/src/DataTable/selection/ControlledSelectionStatus.jsx +++ b/src/DataTable/selection/ControlledSelectionStatus.jsx @@ -55,7 +55,7 @@ ControlledSelectionStatus.propTypes = { /** A class name to append to the base element */ className: PropTypes.string, /** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */ - clearSelectionText: PropTypes.string, + clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), }; export default ControlledSelectionStatus; diff --git a/src/DataTable/tests/FilterStatus.test.jsx b/src/DataTable/tests/FilterStatus.test.jsx index cdc3ac1e5a..5c5accae7e 100644 --- a/src/DataTable/tests/FilterStatus.test.jsx +++ b/src/DataTable/tests/FilterStatus.test.jsx @@ -18,12 +18,6 @@ const filterProps = { showFilteredFields: true, }; -const filterPropsNoFiltered = { - ...filterProps, - showFilteredFields: false, - clearFiltersText: '', -}; - // eslint-disable-next-line react/prop-types const FilterStatusWrapper = ({ value, props }) => ( @@ -61,8 +55,4 @@ describe('', () => { const wrapper = mount(); expect(wrapper.text()).toEqual(''); }); - it('hides filter text', () => { - const wrapper = mount(); - expect(wrapper.text()).toEqual(''); - }); }); diff --git a/src/Toast/Toast.test.jsx b/src/Toast/Toast.test.jsx index 8ee23b78e3..68856c5f38 100644 --- a/src/Toast/Toast.test.jsx +++ b/src/Toast/Toast.test.jsx @@ -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 }) => ( + + + {children} + + +); + describe('', () => { const onCloseHandler = () => {}; const props = { @@ -10,7 +20,7 @@ describe('', () => { }; it('renders optional action as link', () => { const wrapper = mount(( - ', () => { }} > Success message. - )); + )); const toastLink = wrapper.find('a.btn'); expect(toastLink).toHaveLength(1); }); it('renders optional action as button', () => { const wrapper = mount(( - ', () => { }} > Success message. - )); + )); 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(( - Success message. - )); + )); wrapper.prop('onMouseOver'); setTimeout(() => { const toast = wrapper.find(Toast); @@ -58,11 +68,11 @@ describe('', () => { }); it('autohide is set to false onFocus and true onBlur', () => { const wrapper = mount(( - Success message. - )); + )); wrapper.prop('onFocus'); setTimeout(() => { const toast = wrapper.find(Toast); diff --git a/src/Toast/index.jsx b/src/Toast/index.jsx index f2ccf8e7e8..9d0f3a0390 100644 --- a/src/Toast/index.jsx +++ b/src/Toast/index.jsx @@ -103,7 +103,9 @@ Toast.propTypes = { delay: PropTypes.number, /** Class names for the `BaseToast` component */ className: PropTypes.string, - intl: PropTypes.shape.isRequired, + intl: PropTypes.shape({ + formatMessage: PropTypes.func, + }).isRequired, }; export default injectIntl(Toast); diff --git a/www/src/components/Settings.jsx b/www/src/components/Settings.jsx index 37066eb6ee..e80a63fed2 100644 --- a/www/src/components/Settings.jsx +++ b/www/src/components/Settings.jsx @@ -4,6 +4,7 @@ import { Form, Icon, IconButton, + Stack, } from '~paragon-react'; import { Close } from '~paragon-icons'; @@ -39,7 +40,7 @@ const Settings = () => { size="sm" /> -
+ { )} -
+ ); }; From cf579360b9754d50cdeebd6a37cf5f5f3ff5a977 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Tue, 22 Mar 2022 18:47:56 +0200 Subject: [PATCH 03/10] build: add transifex scripts --- .tx/config | 8 ++++++++ Makefile | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 .tx/config diff --git a/.tx/config b/.tx/config new file mode 100644 index 0000000000..8ace9fe7b7 --- /dev/null +++ b/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[edx-platform.paragon] +file_filter = src/i18n/messages/.json +source_file = src/i18n/transifex_input.json +source_lang = en +type = STRUCTURED_JSON \ No newline at end of file diff --git a/Makefile b/Makefile index 57954bac40..930d1e7324 100644 --- a/Makefile +++ b/Makefile @@ -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 is-es5 + +.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 \ No newline at end of file diff --git a/package.json b/package.json index bdf3ab9b84..221470e88d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -75,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.2", "@semantic-release/changelog": "^5.0.1", "@semantic-release/git": "^9.0.1", "@testing-library/jest-dom": "^5.16.3", From 89a1be3fbb6436edc6ca4fb46bdeb6af3e5dec16 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Fri, 25 Mar 2022 12:44:52 +0200 Subject: [PATCH 04/10] docs: update readme with i18n information --- README.md | 104 +++++++++++++++++++++++++++ docs/decisions/0015-i18n-support.rst | 4 +- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2f6bdaa38..23a441e14a 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,110 @@ 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( + + + , + document.getElementById('root') + ) +``` + +Note that if you are using ``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( + + + , + 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'; + + + ``` + + - For places where the display string has to be a plain JavaScript string use ``formatMessage`` (this would require you to use ``injectIntl`` helper, see [Toast](src/Toast/index.jsx) component for a full example), e.g. + + ```javascript + import { injectIntl } from 'react-intl'; + + const MyComponent = ({ altText, intl }) => { + const intlAltText = altText || intl.formatMessage({ + id: 'pgn.MyComponent.altText', + defaultMessage: 'Close', + description: 'Close label for Toast component', + }); + return ( + {}} + variant="primary" + /> + ) + } + + export default injectIntl(MyComponent); + ``` + + **Notes on the format above**: + - `id` is required and must be a dot-separated string of the format `pgn...` + - 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).): diff --git a/docs/decisions/0015-i18n-support.rst b/docs/decisions/0015-i18n-support.rst index a216b8b902..48f3036013 100644 --- a/docs/decisions/0015-i18n-support.rst +++ b/docs/decisions/0015-i18n-support.rst @@ -56,10 +56,10 @@ Consequences ReactDOM.render( - , + , document.getElementById('root') ) - ) + }) initialize({ // this will add your app's messages as well as Paragon's messages to your app From 1def2860598336415ab11497986e12616404a56b Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Fri, 1 Apr 2022 08:52:57 +0300 Subject: [PATCH 05/10] chore: small miscellaneous fixes / improvements --- .gitignore | 1 + .tx/config | 2 +- Makefile | 4 ++-- README.md | 2 +- src/DataTable/FilterStatus.jsx | 16 +++++++++------- src/DataTable/tests/FilterStatus.test.jsx | 9 +++++++++ www/src/components/PropsTable.jsx | 6 +++++- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index b8e50a4450..723ec08469 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log coverage jest* dist +src/i18n/transifex_input.json # gatsby files www/.cache/ diff --git a/.tx/config b/.tx/config index 8ace9fe7b7..84dc667963 100644 --- a/.tx/config +++ b/.tx/config @@ -5,4 +5,4 @@ host = https://www.transifex.com file_filter = src/i18n/messages/.json source_file = src/i18n/transifex_input.json source_lang = en -type = STRUCTURED_JSON \ No newline at end of file +type = STRUCTURED_JSON diff --git a/Makefile b/Makefile index 930d1e7324..8ca1c9c5f4 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ transifex_langs = "ar,fr,es_419,zh_CN" i18n = ./src/i18n transifex_input = $(i18n)/transifex_input.json -NPM_TESTS=build i18n_extract lint test is-es5 +NPM_TESTS=build i18n_extract lint test .PHONY: test test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite @@ -49,4 +49,4 @@ pull_translations: # 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 \ No newline at end of file + git diff --exit-code package-lock.json diff --git a/README.md b/README.md index 23a441e14a..f67a3a1407 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Since we are using ``react-intl`` that means that your whole app needs to be wra ) ``` -Note that if you are using ``frontend-platform``'s ``AppProvider`` component you don't need a separate context, +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 diff --git a/src/DataTable/FilterStatus.jsx b/src/DataTable/FilterStatus.jsx index f4f922f3b3..ac9682c64b 100644 --- a/src/DataTable/FilterStatus.jsx +++ b/src/DataTable/FilterStatus.jsx @@ -24,13 +24,15 @@ const FilterStatus = ({ size={size} onClick={() => setAllFilters([])} > - {clearFiltersText || ( - - )} + {clearFiltersText === undefined + ? ( + + ) + : clearFiltersText} ); diff --git a/src/DataTable/tests/FilterStatus.test.jsx b/src/DataTable/tests/FilterStatus.test.jsx index 5c5accae7e..e3ceccede9 100644 --- a/src/DataTable/tests/FilterStatus.test.jsx +++ b/src/DataTable/tests/FilterStatus.test.jsx @@ -17,6 +17,11 @@ const filterProps = { className: 'filterClass', showFilteredFields: true, }; +const filterPropsNoFiltered = { + ...filterProps, + showFilteredFields: false, + clearFiltersText: '', +}; // eslint-disable-next-line react/prop-types const FilterStatusWrapper = ({ value, props }) => ( @@ -55,4 +60,8 @@ describe('', () => { const wrapper = mount(); expect(wrapper.text()).toEqual(''); }); + it('hides filter text', () => { + const wrapper = mount(); + expect(wrapper.text()).toEqual(''); + }); }); diff --git a/www/src/components/PropsTable.jsx b/www/src/components/PropsTable.jsx index 7c7aa1a441..51888b2ce5 100644 --- a/www/src/components/PropsTable.jsx +++ b/www/src/components/PropsTable.jsx @@ -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 &&
{content}
} {componentProps.length > 0 ? (
    - {componentProps.map(metadata => metadata.name !== 'intl' && )} + {componentProps + .filter(metadata => !IGNORED_COMPONENT_PROPS.includes(metadata.name)) + .map(metadata => )}
) :
This component does not receive any props.
} From 2924e1103a4abcb29d7f27766ee4af0f8786a9f4 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Fri, 1 Apr 2022 09:03:18 +0300 Subject: [PATCH 06/10] chore: remove test translations --- src/i18n/messages/ar.json | 4 +--- src/i18n/messages/ca.json | 4 +--- src/i18n/messages/es_419.json | 4 +--- src/i18n/messages/fr.json | 4 +--- src/i18n/messages/he.json | 4 +--- src/i18n/messages/id.json | 4 +--- src/i18n/messages/ko_KR.json | 4 +--- src/i18n/messages/pl.json | 4 +--- src/i18n/messages/pt_BR.json | 4 +--- src/i18n/messages/ru.json | 4 +--- src/i18n/messages/th.json | 4 +--- src/i18n/messages/uk.json | 4 +--- src/i18n/messages/zh_CN.json | 4 +--- 13 files changed, 13 insertions(+), 39 deletions(-) diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 6f9bf16de0..9e26dfeeb6 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Arabic" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/ca.json b/src/i18n/messages/ca.json index e09e572888..9e26dfeeb6 100644 --- a/src/i18n/messages/ca.json +++ b/src/i18n/messages/ca.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Catalan" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 2ac25c9c47..9e26dfeeb6 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Spanish" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 3717671714..9e26dfeeb6 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "French" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 8d125fada5..9e26dfeeb6 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Hebrew" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 2321c1256d..9e26dfeeb6 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Indonesian" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/ko_KR.json b/src/i18n/messages/ko_KR.json index 887d526c02..9e26dfeeb6 100644 --- a/src/i18n/messages/ko_KR.json +++ b/src/i18n/messages/ko_KR.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Korean" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 4bedc0d200..9e26dfeeb6 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Polish" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/pt_BR.json b/src/i18n/messages/pt_BR.json index 6f6415b157..9e26dfeeb6 100644 --- a/src/i18n/messages/pt_BR.json +++ b/src/i18n/messages/pt_BR.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Portuguese" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 9950a0f142..9e26dfeeb6 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Russian" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 23191fa88a..9e26dfeeb6 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Thai" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 6c6842993e..9e26dfeeb6 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Ukrainian" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 8995011316..9e26dfeeb6 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -1,3 +1 @@ -{ - "pgn.Alert.closeLabel": "Chinese" -} \ No newline at end of file +{} \ No newline at end of file From 1d4ea68a91b483b1bc818918b80ad4e376682472 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Mon, 9 May 2022 09:28:26 +0300 Subject: [PATCH 07/10] feat: upgrade to react-intl@5 --- README.md | 72 ++++++++++++++++++++--------- package.json | 4 +- src/Alert/index.jsx | 2 +- src/Toast/index.jsx | 10 ++-- www/src/components/CodeBlock.jsx | 7 ++- www/src/context/SettingsContext.jsx | 34 +------------- 6 files changed, 62 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index f67a3a1407..ff6630b098 100644 --- a/README.md +++ b/README.md @@ -235,28 +235,58 @@ When developing a new component you should generally follow three rules: /> ``` - - For places where the display string has to be a plain JavaScript string use ``formatMessage`` (this would require you to use ``injectIntl`` helper, see [Toast](src/Toast/index.jsx) component for a full example), e.g. + - 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. - ```javascript - import { injectIntl } from 'react-intl'; - - const MyComponent = ({ altText, intl }) => { - const intlAltText = altText || intl.formatMessage({ - id: 'pgn.MyComponent.altText', - defaultMessage: 'Close', - description: 'Close label for Toast component', - }); - return ( - {}} - variant="primary" - /> - ) - } - - export default injectIntl(MyComponent); - ``` + - 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 ( + {}} + 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 ( + {}} + variant="primary" + /> + ) + + export default MyFunctionComponent; + ``` **Notes on the format above**: - `id` is required and must be a dot-separated string of the format `pgn...` diff --git a/package.json b/package.json index 221470e88d..53e29b4b52 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "prop-types": "^15.8.1", "react-bootstrap": "^1.6.4", "react-focus-on": "^3.5.4", - "react-intl": "2.9.0", + "react-intl": "^5.25.0", "react-popper": "^2.2.5", "react-proptype-conditional-require": "^1.0.4", "react-responsive": "^8.2.0", @@ -76,7 +76,7 @@ "@babel/preset-env": "^7.16.8", "@babel/preset-react": "^7.16.7", "@edx/eslint-config": "^3.0.0", - "@formatjs/cli": "^4.8.2", + "@formatjs/cli": "^4.8.4", "@semantic-release/changelog": "^5.0.1", "@semantic-release/git": "^9.0.1", "@testing-library/jest-dom": "^5.16.3", diff --git a/src/Alert/index.jsx b/src/Alert/index.jsx index 5f37ad4cfb..93a5f7e121 100644 --- a/src/Alert/index.jsx +++ b/src/Alert/index.jsx @@ -70,7 +70,7 @@ const Alert = React.forwardRef(({ {closeLabel || ( )} diff --git a/src/Toast/index.jsx b/src/Toast/index.jsx index 9d0f3a0390..237568a3ed 100644 --- a/src/Toast/index.jsx +++ b/src/Toast/index.jsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import BaseToast from 'react-bootstrap/Toast'; -import { injectIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import ToastContainer from './ToastContainer'; import { Button, IconButton, Icon } from '..'; @@ -13,8 +13,9 @@ export const TOAST_CLOSE_LABEL_TEXT = 'Close'; export const TOAST_DELAY = 5000; function Toast({ - action, children, className, closeLabel, onClose, show, intl, ...rest + action, children, className, closeLabel, onClose, show, ...rest }) { + const intl = useIntl(); const [autoHide, setAutoHide] = useState(true); const intlCloseLabel = closeLabel || intl.formatMessage({ id: 'pgn.Toast.closeLabel', @@ -103,9 +104,6 @@ Toast.propTypes = { delay: PropTypes.number, /** Class names for the `BaseToast` component */ className: PropTypes.string, - intl: PropTypes.shape({ - formatMessage: PropTypes.func, - }).isRequired, }; -export default injectIntl(Toast); +export default Toast; diff --git a/www/src/components/CodeBlock.jsx b/www/src/components/CodeBlock.jsx index ba52894660..10c46b43e3 100644 --- a/www/src/components/CodeBlock.jsx +++ b/www/src/components/CodeBlock.jsx @@ -10,7 +10,7 @@ import theme from 'prism-react-renderer/themes/duotoneDark'; import { LiveProvider, LiveEditor, LiveError, LivePreview, } from 'react-live'; -import { FormattedMessage, injectIntl } from 'react-intl'; +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'; @@ -46,8 +46,8 @@ function CodeBlock({ children, className, live, - intl, }) { + const intl = useIntl(); const language = className ? className.replace(/language-/, '') : 'jsx'; if (live) { @@ -108,7 +108,6 @@ CodeBlock.propTypes = { children: PropTypes.string.isRequired, className: PropTypes.string, live: PropTypes.bool, - intl: PropTypes.shape({}).isRequired, }; CodeBlock.defaultProps = { @@ -116,4 +115,4 @@ CodeBlock.defaultProps = { className: '', }; -export default injectIntl(CodeBlock); +export default CodeBlock; diff --git a/www/src/context/SettingsContext.jsx b/www/src/context/SettingsContext.jsx index ace638ac21..8a456b895c 100644 --- a/www/src/context/SettingsContext.jsx +++ b/www/src/context/SettingsContext.jsx @@ -1,43 +1,11 @@ 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 { IntlProvider, addLocaleData } from 'react-intl'; -import arLocale from 'react-intl/locale-data/ar'; -import enLocale from 'react-intl/locale-data/en'; -import esLocale from 'react-intl/locale-data/es'; -import frLocale from 'react-intl/locale-data/fr'; -import zhLocale from 'react-intl/locale-data/zh'; -import caLocale from 'react-intl/locale-data/ca'; -import heLocale from 'react-intl/locale-data/he'; -import idLocale from 'react-intl/locale-data/id'; -import koLocale from 'react-intl/locale-data/ko'; -import plLocale from 'react-intl/locale-data/pl'; -import ptLocale from 'react-intl/locale-data/pt'; -import ruLocale from 'react-intl/locale-data/ru'; -import thLocale from 'react-intl/locale-data/th'; -import ukLocale from 'react-intl/locale-data/uk'; - import { THEMES } from '../../theme-config'; -addLocaleData([ - ...arLocale, - ...caLocale, - ...heLocale, - ...idLocale, - ...plLocale, - ...ruLocale, - ...thLocale, - ...ukLocale, - ...enLocale, - ...esLocale, - ...frLocale, - ...zhLocale, - ...koLocale, - ...ptLocale, -]); - const defaultValue = { settings: {}, handleSettingsChange: () => {}, From a15a66ce38e4dd87da5a5578555472846c9c81a8 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Tue, 10 May 2022 11:32:49 +0300 Subject: [PATCH 08/10] test: add IntlProvider wrapper to tests which include i18n components --- src/Alert/Alert.test.jsx | 28 +++++++---- src/Alert/__snapshots__/Alert.test.jsx.snap | 12 ++--- .../tests/ControlledSelectionStatus.test.jsx | 7 ++- .../selection/tests/SelectionStatus.test.jsx | 7 ++- src/DataTable/tests/DataTable.test.jsx | 38 +++++++++------ src/DataTable/tests/ExpandAll.test.jsx | 11 ++++- src/DataTable/tests/RowStatus.test.jsx | 8 +++- src/DataTable/tests/SmartStatus.test.jsx | 8 +++- www/gatsby-node.js | 1 + www/package-lock.json | 46 ------------------- www/package.json | 1 - 11 files changed, 82 insertions(+), 85 deletions(-) diff --git a/src/Alert/Alert.test.jsx b/src/Alert/Alert.test.jsx index b8743c801c..6361ce39a3 100644 --- a/src/Alert/Alert.test.jsx +++ b/src/Alert/Alert.test.jsx @@ -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 }) => ( + + + {children} + + +); + describe('', () => { it('renders without any props', () => { const tree = renderer.create(( - Alert + Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders with icon prop', () => { const tree = renderer.create(( - Alert + Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders with dismissible prop', () => { const tree = renderer.create(( - Alert + Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); it('handles dismissible onClose', () => { const mockOnClose = jest.fn(); const wrapper = mount(( - Alert + Alert )); wrapper.find('.btn').simulate('click'); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('renders with button prop', () => { const tree = renderer.create(( - Hello]}>Alert + Hello]}>Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); it('handles button onClick', () => { const mockOnClick = jest.fn(); const wrapper = mount(( - Hello]}>Alert + Hello]}>Alert )); wrapper.find('.btn').simulate('click'); expect(mockOnClick).toHaveBeenCalledTimes(1); }); it('renders with button and dismissible props', () => { const tree = renderer.create(( - Hello]} dismissible>Alert + Hello]} dismissible>Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders with stacked prop', () => { const tree = renderer.create(( - Hello]} dismissible>Alert + Hello]} dismissible>Alert )).toJSON(); expect(tree).toMatchSnapshot(); }); @@ -65,7 +75,7 @@ describe('', () => { act(() => { tree = renderer.create(( - Alert + Alert )).toJSON(); }); diff --git a/src/Alert/__snapshots__/Alert.test.jsx.snap b/src/Alert/__snapshots__/Alert.test.jsx.snap index 40d6111bfb..577697ee78 100644 --- a/src/Alert/__snapshots__/Alert.test.jsx.snap +++ b/src/Alert/__snapshots__/Alert.test.jsx.snap @@ -25,9 +25,7 @@ exports[` renders with button and dismissible props 1`] = ` onClick={[Function]} type="button" > - - Dismiss - + Dismiss @@ -167,9 +163,7 @@ exports[` renders with stacked prop 1`] = ` onClick={[Function]} type="button" > - - Dismiss - + Dismiss ) : ( @@ -18,7 +18,7 @@ const ExpandAll = ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }) => ( )} diff --git a/src/DataTable/selection/BaseSelectionStatus.jsx b/src/DataTable/selection/BaseSelectionStatus.jsx index 1269aa7a93..4e20279f59 100644 --- a/src/DataTable/selection/BaseSelectionStatus.jsx +++ b/src/DataTable/selection/BaseSelectionStatus.jsx @@ -51,7 +51,7 @@ const BaseSelectionStatus = ({ )} @@ -68,7 +68,7 @@ const BaseSelectionStatus = ({ )} diff --git a/src/DataTable/tests/FilterStatus.test.jsx b/src/DataTable/tests/FilterStatus.test.jsx index e3ceccede9..24bd5aafdd 100644 --- a/src/DataTable/tests/FilterStatus.test.jsx +++ b/src/DataTable/tests/FilterStatus.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import { IntlProvider } from 'react-intl'; import FilterStatus from '../FilterStatus'; import { Button } from '../..'; @@ -25,7 +26,11 @@ const filterPropsNoFiltered = { // eslint-disable-next-line react/prop-types const FilterStatusWrapper = ({ value, props }) => ( - + + + + + ); describe('', () => { diff --git a/www/gatsby-node.js b/www/gatsby-node.js index a1776c1849..cdd16a0fb3 100644 --- a/www/gatsby-node.js +++ b/www/gatsby-node.js @@ -28,7 +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/'), + 'react-intl': path.resolve(__dirname, 'node_modules/react-intl/'), }, }, }) diff --git a/www/package-lock.json b/www/package-lock.json index 40ce90006e..9cd385b0a1 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -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", diff --git a/www/package.json b/www/package.json index b03fee0bd6..d6e390f847 100644 --- a/www/package.json +++ b/www/package.json @@ -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", diff --git a/www/src/components/Settings.jsx b/www/src/components/Settings.jsx index e80a63fed2..719d94e629 100644 --- a/www/src/components/Settings.jsx +++ b/www/src/components/Settings.jsx @@ -18,8 +18,6 @@ const Settings = () => { handleSettingsChange, showSettings, closeSettings, - direction, - onDirectionChange, } = useContext(SettingsContext); return ( @@ -61,8 +59,8 @@ const Settings = () => { handleSettingsChange('direction', e.target.value)} floatingLabel="Direction" >