I18n example (#33)

* feat: i18n pt 1

* fix: prevent float calculations when updating dimensions
This commit is contained in:
Ben Warzeski
2022-03-17 14:08:46 -04:00
committed by GitHub
parent a79da4cb2d
commit 3b8a7780ac
18 changed files with 160 additions and 94 deletions

75
package-lock.json generated
View File

@@ -24961,6 +24961,34 @@
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"intl-messageformat": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
"integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=",
"dev": true,
"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=",
"dev": true
},
"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==",
"dev": true,
"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"
}
}
}
},
@@ -32562,23 +32590,6 @@
"integrity": "sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==",
"dev": true
},
"intl-messageformat": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
"integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=",
"dev": true,
"requires": {
"intl-messageformat-parser": "1.4.0"
},
"dependencies": {
"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=",
"dev": true
}
}
},
"intl-messageformat-parser": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz",
@@ -32595,6 +32606,23 @@
"dev": true,
"requires": {
"intl-messageformat": "^2.0.0"
},
"dependencies": {
"intl-messageformat": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
"integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=",
"dev": true,
"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=",
"dev": true
}
}
},
"into-stream": {
@@ -37492,19 +37520,6 @@
"use-sidecar": "^1.0.5"
}
},
"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==",
"dev": true,
"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": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -70,8 +70,8 @@
"tinymce": "^5.10.2"
},
"peerDependencies": {
"@edx/frontend-platform": "^1.8.0",
"@edx/paragon": ">= 7.0.0 < 20.0.0",
"@edx/frontend-platform": "1.14.0",
"prop-types": "^15.5.10",
"react": "^16.14.0",
"react-dom": "^16.14.0"

View File

@@ -28,7 +28,11 @@ exports[`EditorFooter snapshots Save Failed, error message raised 1`] = `
}
variant="tertiary"
>
Cancel
<FormattedMessage
defaultMessage="Cancel"
description="Label for cancel button"
id="authoring.editorfooter.cancelButton.label"
/>
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
@@ -72,7 +76,11 @@ exports[`EditorFooter snapshots not intialized, Spinner appears and button is di
}
variant="tertiary"
>
Cancel
<FormattedMessage
defaultMessage="Cancel"
description="Label for cancel button"
id="authoring.editorfooter.cancelButton.label"
/>
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
@@ -115,7 +123,11 @@ exports[`EditorFooter snapshots renders as expected with default behavior 1`] =
}
variant="tertiary"
>
Cancel
<FormattedMessage
defaultMessage="Cancel"
description="Label for cancel button"
id="authoring.editorfooter.cancelButton.label"
/>
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"

View File

@@ -9,13 +9,13 @@ import {
ModalDialog,
Toast,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { nullMethod, saveBlock, navigateCallback } from '../../hooks';
import { RequestKeys } from '../../data/constants/requests';
import { selectors, thunkActions } from '../../data/redux';
import messages from '../messages';
import messages from './messages';
import * as module from '.';
export const handleSaveClicked = (props) => () => saveBlock(props);
@@ -23,6 +23,8 @@ export const handleCancelClicked = ({ returnUrl }) => navigateCallback(returnUrl
export const EditorFooter = ({
editorRef,
// injected
intl,
// redux
isInitialized,
returnUrl,
@@ -38,14 +40,14 @@ export const EditorFooter = ({
<ActionRow>
<ActionRow.Spacer />
<Button
aria-label="Discard Changes and Return to Learning Context"
aria-label={intl.formatMessage(messages.cancelButtonAriaLabel)}
variant="tertiary"
onClick={module.handleCancelClicked({ returnUrl })}
>
Cancel
<FormattedMessage {...messages.cancelButtonLabel} />
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
aria-label={intl.formatMessage(messages.saveButtonAriaLabel)}
onClick={module.handleSaveClicked({
editorRef,
returnUrl,
@@ -70,6 +72,8 @@ EditorFooter.propTypes = {
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]),
// injected
intl: intlShape.isRequired,
// redux
isInitialized: PropTypes.bool.isRequired,
returnUrl: PropTypes.string,
@@ -88,4 +92,4 @@ export const mapDispatchToProps = {
saveBlockContent: thunkActions.app.saveBlock,
};
export default connect(mapStateToProps, mapDispatchToProps)(EditorFooter);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditorFooter));

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as module from './index';
import { formatMessage } from '../../../testUtils';
import { selectors, thunkActions } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import { saveBlock, navigateCallback } from '../../hooks';
import * as module from './index';
jest.mock('../../data/redux', () => ({
thunkActions: {
@@ -39,6 +41,7 @@ jest.mock('../../hooks', () => ({
describe('EditorFooter', () => {
const props = {
intl: { formatMessage },
editorRef: jest.fn().mockName('args.editorRef'),
isInitialized: true,
returnUrl: 'hocuspocus.ca',

View File

@@ -0,0 +1,29 @@
export const messages = {
contentSaveFailed: {
id: 'authoring.editorfooter.save.error',
defaultMessage: 'Error: Content save failed. Try again later.',
description: 'Error message displayed when content fails to save.',
},
cancelButtonAriaLabel: {
id: 'authoring.editorfooter.cancelButton.ariaLabel',
defaultMessage: 'Discard Changes and Return to Learning Context',
description: 'Aria label for cancel button',
},
cancelButtonLabel: {
id: 'authoring.editorfooter.cancelButton.label',
defaultMessage: 'Cancel',
description: 'Label for cancel button',
},
saveButtonAriaLabel: {
id: 'authoring.editorfooter.savebutton.ariaLabel',
defaultMessage: 'Save Changes and Return to Learning Context',
description: 'Aria label for save button',
},
addToCourse: {
id: 'authoring.editorfooter.savebutton.label',
defaultMessage: 'Save',
description: 'Label for Save button',
},
};
export default messages;

View File

@@ -4,20 +4,21 @@ import PropTypes from 'prop-types';
import { Icon, IconButton } from '@edx/paragon';
import { Edit } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { actions, selectors } from '../../data/redux';
import { localTitleHooks } from './hooks';
import messages from '../messages';
import messages from './messages';
import EditableHeader from './EditableHeader';
export const HeaderTitle = ({
editorRef,
intl,
isInitialized,
setBlockTitle,
typeHeader,
}) => {
if (!isInitialized) { return <FormattedMessage {...messages.loading} />; }
if (!isInitialized) { return intl.formatMessage(messages.loading); }
const {
inputRef,
isEditing,
@@ -51,8 +52,7 @@ export const HeaderTitle = ({
{localTitle}
</div>
<IconButton
alt="Edit"
aria-label="Edit Title"
alt={intl.formatMessage(messages.editTitleLabel)}
className="mr-2"
iconAs={Icon}
onClick={startEditing}
@@ -70,6 +70,8 @@ HeaderTitle.propTypes = {
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]),
// injected
intl: intlShape.isRequired,
// redux
isInitialized: PropTypes.bool.isRequired,
setBlockTitle: PropTypes.func.isRequired,
@@ -85,4 +87,4 @@ export const mapDispatchToProps = {
setBlockTitle: actions.app.setBlockTitle,
};
export default connect(mapStateToProps, mapDispatchToProps)(HeaderTitle);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(HeaderTitle));

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as module from './HeaderTitle';
import { formatMessage } from '../../../testUtils';
import { actions, selectors } from '../../data/redux';
import { localTitleHooks } from './hooks';
import * as module from './HeaderTitle';
jest.mock('../../data/redux', () => ({
actions: {
@@ -25,6 +26,7 @@ jest.mock('./hooks', () => ({
describe('HeaderTitle', () => {
const props = {
intl: { formatMessage },
editorRef: jest.fn().mockName('args.editorRef'),
isInitialized: false,
setBlockTitle: jest.fn().mockName('args.setBlockTitle'),

View File

@@ -25,8 +25,7 @@ exports[`HeaderTitle snapshots initialized 1`] = `
TeST LocALtitLE
</div>
<IconButton
alt="Edit"
aria-label="Edit Title"
alt="Edit Title"
className="mr-2"
iconAs="Icon"
onClick={[MockFunction localTitleHooks.startEditing]}
@@ -36,10 +35,4 @@ exports[`HeaderTitle snapshots initialized 1`] = `
</div>
`;
exports[`HeaderTitle snapshots not initialized 1`] = `
<FormattedMessage
defaultMessage="Loading..."
description="Message displayed while loading content"
id="authoring.texteditor.title.loading"
/>
`;
exports[`HeaderTitle snapshots not initialized 1`] = `"Loading..."`;

View File

@@ -11,8 +11,7 @@ exports[` 1`] = `
</ModalDialog.Title>
<ActionRow.Spacer />
<IconButton
alt="Close"
aria-label="Cancel Changes and Return to Learning Context"
alt="Cancel Changes and Return to Learning Context"
className="mr-2"
iconAs="Icon"
src={[MockFunction icons.Close]}

View File

@@ -9,23 +9,24 @@ import {
ModalDialog,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { selectors } from '../../data/redux';
import * as appHooks from '../../hooks';
import HeaderTitle from './HeaderTitle';
import messages from './messages';
export const EditorHeader = ({ returnUrl }) => (
export const EditorHeader = ({ intl, returnUrl }) => (
<div className="editor-header">
<ModalDialog.Header>
<ActionRow>
<ModalDialog.Title><HeaderTitle /></ModalDialog.Title>
<ActionRow.Spacer />
<IconButton
aria-label="Cancel Changes and Return to Learning Context"
alt={intl.formatMessage(messages.cancelChangesLabel)}
src={Close}
iconAs={Icon}
alt="Close"
onClick={appHooks.navigateCallback(returnUrl)}
variant="light"
className="mr-2"
@@ -35,10 +36,13 @@ export const EditorHeader = ({ returnUrl }) => (
</div>
);
EditorHeader.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
returnUrl: PropTypes.string.isRequired,
};
export const mapStateToProps = (state) => ({
returnUrl: selectors.app.returnUrl(state),
});
export default connect(mapStateToProps)(EditorHeader);
export default injectIntl(connect(mapStateToProps)(EditorHeader));

View File

@@ -2,9 +2,11 @@ import React from 'react';
import { shallow } from 'enzyme';
import { IconButton } from '@edx/paragon';
import * as module from './index';
import { formatMessage } from '../../../testUtils';
import { selectors } from '../../data/redux';
import * as appHooks from '../../hooks';
import * as module from './index';
jest.mock('.', () => ({
__esModule: true, // Use it when dealing with esModules
@@ -28,6 +30,7 @@ jest.mock('./HeaderTitle', () => 'HeaderTitle');
describe('Editor Header index', () => {
const props = {
intl: { formatMessage },
returnUrl: 'TeST-ReTurNurL',
};
const { EditorHeader } = module;

View File

@@ -0,0 +1,19 @@
export const messages = {
loading: {
id: 'authoring.texteditor.title.loading',
description: 'Message displayed while loading content',
defaultMessage: 'Loading...',
},
cancelChangesLabel: {
id: 'authoring.texteditor.header.cancelChangesLabel',
defaultMessage: 'Cancel Changes and Return to Learning Context',
description: 'Aria label title for icon button to return to learning context',
},
editTitleLabel: {
id: 'authoring.texteditor.header.editTitleLabel',
defaultMessage: 'Edit Title',
description: 'Aria label title for icon button to edit the xblock title',
},
};
export default messages;

View File

@@ -1,19 +0,0 @@
export const messages = {
contentSaveFailed: {
id: 'authoring.editorfooter.save.error',
defaultMessage: 'Error: Content save failed. Try again later.',
description: 'Error message displayed when content fails to save.',
},
addToCourse: {
id: 'authoring.editorfooter.savebutton.label',
defaultMessage: 'Save',
description: 'Label for Save button',
},
loading: {
id: 'authoring.texteditor.title.loading',
description: 'Message displayed while loading content',
defaultMessage: 'Loading...',
},
};
export default messages;

View File

@@ -59,8 +59,8 @@ export const getValidDimensions = ({
// if closest valid iteration is current iteration, move one iteration in the change direction
if (iter === (dimensions[keys.changed] / minInc[keys.changed])) { iter += direction; }
out[keys.changed] = iter * minInc[keys.changed];
out[keys.other] = out[keys.changed] * (locked[keys.other] / locked[keys.changed]);
out[keys.changed] = Math.round(iter * minInc[keys.changed]);
out[keys.other] = Math.round(out[keys.changed] * (locked[keys.other] / locked[keys.changed]));
return out;
};

14
www/package-lock.json generated
View File

@@ -17564,9 +17564,9 @@
}
},
"@edx/frontend-platform": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.15.0.tgz",
"integrity": "sha512-fnbxgDE9eGUfE48elFWtKiqGdp0tLg2y6q6e0CWmi5pMNNPORWQ/w/gBKMgk3QOZL7HuKmSQafrzCI6A0nQC2Q==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.14.0.tgz",
"integrity": "sha512-fxqjZ948c6eGu1TanyNiHc2X44u8kByDzJv+vw/qPa6DtbgB/F520b6Xkq3X90nRg8jPgLYa8XQPkoLGF4v7AQ==",
"requires": {
"@cospired/i18n-iso-languages": "2.2.0",
"axios": "0.21.4",
@@ -17582,7 +17582,7 @@
"lodash.memoize": "4.1.2",
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
"pubsub-js": "1.9.3",
"react-intl": "2.9.0",
"universal-cookie": "4.0.4"
},
@@ -27935,9 +27935,9 @@
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"pubsub-js": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz",
"integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A=="
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.3.tgz",
"integrity": "sha512-FhYYlPNOywTh7zN38u5AlG67emA47w6JZd7YgdQU1w8gQbZhhIGxVM0AQosdaINHb2ALb+fhfnVyBJAt4D4IzA=="
},
"pump": {
"version": "3.0.0",

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-build": "^9.1.1",
"@edx/frontend-platform": "^1.15.0",
"@edx/frontend-platform": "1.14.0",
"@edx/frontend-lib-content-components": "file:..",
"@edx/paragon": "16.17.0",
"core-js": "^3.21.1",

View File

@@ -4,7 +4,7 @@ import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import { IntlProvider } from 'react-intl';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import messages from './i18n';
import './index.scss';