Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: change lang selector logic #493

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,19 @@ This library has the following exports:
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
* ``dist/footer.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.

<Footer /> component props
==========================
Language Selector
-----------------

* onLanguageSelected: Provides the footer with an event handler for when the user selects a
language from its dropdown.
* supportedLanguages: An array of objects representing available languages. See example below for object shape.
The language selector dropdown is optional and can be enabled by setting the MFE configuration variable ``ENABLE_FOOTER_LANG_SELECTOR`` to ``true``.
Secondly, configue the languages that should be displayed in the dropdown by setting the MFE configuration variable ``SITE_SUPPORTED_LANGUAGES`` to an array of locale languages.
Example:

.. code-block:: python

MFE_CONFIG["EDX_FRONTEND_APP_CONFIG"] = {
"ENABLE_FOOTER_LANG_SELECTOR": True,
"SITE_SUPPORTED_LANGUAGES": ['en', 'es', 'fr', 'pt-br'],
}

Plugin
======
Expand All @@ -108,13 +115,7 @@ Component Usage Example::

...

<Footer
onLanguageSelected={(languageCode) => {/* set language */}}
supportedLanguages={[
{ label: 'English', value: 'en'},
{ label: 'Español', value: 'es' },
]}
/>
<Footer />

* `An example of minimal component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L23>`_
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L9>`_
Expand Down
7 changes: 7 additions & 0 deletions env.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is used only for the example application.
const config = {
ENABLE_FOOTER_LANG_SELECTOR: true,
SITE_SUPPORTED_LANGUAGES: ['es', 'en'],
};

export default config;
8 changes: 1 addition & 7 deletions example/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@ subscribe(APP_READY, () => {
authenticatedUser: null,
config: getConfig(),
}}>
<Footer
onLanguageSelected={() => {}}
supportedLanguages={[
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
]}
/>
<Footer />
</AppContext.Provider>
</AppProvider>,
document.getElementById('root'),
Expand Down
26 changes: 9 additions & 17 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,10 @@ class SiteFooter extends React.Component {

render() {
const {
supportedLanguages,
onLanguageSelected,
logo,
intl,
} = this.props;
const showLanguageSelector = supportedLanguages.length > 0 && onLanguageSelected;
const { config } = this.context;

const { config, authenticatedUser } = this.context;
return (
<footer
role="contentinfo"
Expand All @@ -61,11 +57,14 @@ class SiteFooter extends React.Component {
/>
</a>
<div className="flex-grow-1" />
{showLanguageSelector && (
<LanguageSelector
options={supportedLanguages}
onSubmit={onLanguageSelected}
/>
{config.ENABLE_FOOTER_LANG_SELECTOR && (
<div className="mb-2">
<LanguageSelector
options={config.SITE_SUPPORTED_LANGUAGES}
username={authenticatedUser?.username}
langCookieName={config.LANGUAGE_PREFERENCE_COOKIE_NAME}
/>
</div>
)}
</div>
</footer>
Expand All @@ -78,17 +77,10 @@ SiteFooter.contextType = AppContext;
SiteFooter.propTypes = {
intl: intlShape.isRequired,
logo: PropTypes.string,
onLanguageSelected: PropTypes.func,
supportedLanguages: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
})),
};

SiteFooter.defaultProps = {
logo: undefined,
onLanguageSelected: undefined,
supportedLanguages: [],
};

export default injectIntl(SiteFooter);
Expand Down
40 changes: 10 additions & 30 deletions src/components/Footer.test.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable react/prop-types */
import React, { useMemo } from 'react';
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform/testing';

import Footer from './Footer';

Expand All @@ -27,27 +27,25 @@ const FooterWithContext = ({ locale = 'es' }) => {
);
};

const FooterWithLanguageSelector = ({ languageSelected = () => {} }) => {
const { LANGUAGE_PREFERENCE_COOKIE_NAME } = process.env;
const FooterWithLanguageSelector = ({ authenticatedUser = null }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
authenticatedUser,
config: {
ENABLE_FOOTER_LANG_SELECTOR: true,
LANGUAGE_PREFERENCE_COOKIE_NAME,
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_SUPPORTED_LANGUAGES: ['es', 'en'],
},
}), []);
}), [authenticatedUser]);

return (
<IntlProvider locale="en">
<AppContext.Provider
value={contextValue}
>
<Footer
onLanguageSelected={languageSelected}
supportedLanguages={[
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
]}
/>
<Footer />
</AppContext.Provider>
</IntlProvider>
);
Expand All @@ -68,29 +66,11 @@ describe('<Footer />', () => {
expect(tree).toMatchSnapshot();
});
it('renders with a language selector', () => {
initializeMockApp();
const tree = renderer
.create(<FooterWithLanguageSelector />)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

describe('handles language switching', () => {
it('calls onLanguageSelected prop when a language is changed', () => {
const mockHandleLanguageSelected = jest.fn();
render(<FooterWithLanguageSelector languageSelected={mockHandleLanguageSelected} />);

fireEvent.submit(screen.getByTestId('site-footer-submit-btn'), {
target: {
elements: {
'site-footer-language-select': {
value: 'es',
},
},
},
});

expect(mockHandleLanguageSelected).toHaveBeenCalledWith('es');
});
});
});
58 changes: 0 additions & 58 deletions src/components/LanguageSelector.jsx

This file was deleted.

30 changes: 30 additions & 0 deletions src/components/LanguageSelector/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';

export async function patchPreferences(username, params) {
let processedParams = snakeCaseObject(params);
processedParams = convertKeyNames(processedParams, {
pref_lang: 'pref-lang',
});

await getAuthenticatedHttpClient()
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
}

export async function postSetLang(code) {
const formData = new FormData();
const requestConfig = {
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
};
const url = `${getConfig().LMS_BASE_URL}/i18n/setlang/`;
formData.append('language', code);

await getAuthenticatedHttpClient()
.post(url, formData, requestConfig);
}
82 changes: 82 additions & 0 deletions src/components/LanguageSelector/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { publish } from '@edx/frontend-platform';
import {
injectIntl, LOCALE_CHANGED, getLocale, handleRtl, getPrimaryLanguageSubtag,
} from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { Dropdown, useWindowSize } from '@openedx/paragon';
import { Language } from '@openedx/paragon/icons';
import { getCookies } from '@edx/frontend-platform/i18n/lib';
import { patchPreferences, postSetLang } from './data';

const onLanguageSelected = async ({ langCookieName, username, selectedLlocale }) => {
try {
if (username) {
await patchPreferences(username, { prefLang: selectedLlocale });
await postSetLang(selectedLlocale);
} else {
getCookies().set(langCookieName, selectedLlocale);
}
publish(LOCALE_CHANGED, getLocale());
handleRtl();
} catch (error) {
logError(error);
}
};
const getLocaleName = (locale) => {
const langName = new Intl.DisplayNames([locale], { type: 'language', languageDisplay: 'standard' }).of(locale);
return langName.replace(/^\w/, (c) => c.toUpperCase());
};

const LanguageSelector = ({
langCookieName, options, username,
}) => {
const [currentLocale, setLocale] = useState(getLocale());
const { width } = useWindowSize();

const handleSelect = (selectedLlocale) => {
if (currentLocale !== selectedLlocale) {
onLanguageSelected({ langCookieName, username, selectedLlocale });
}
setLocale(selectedLlocale);
};

const currentLocaleLabel = useMemo(() => {
if (width < 576) {
return '';
}
if (width < 768) {
return getPrimaryLanguageSubtag(currentLocale).toUpperCase();
}
return getLocaleName(currentLocale);
}, [currentLocale, width]);

return (
<Dropdown onSelect={handleSelect}>
<Dropdown.Toggle
id="lang-selector-dropdown"
iconBefore={Language}
variant="outline-primary"
size="sm"
>
{currentLocaleLabel}
</Dropdown.Toggle>
<Dropdown.Menu>
{options.map((locale) => (
<Dropdown.Item key={`lang-selector-${locale}`} eventKey={locale}>
{getLocaleName(locale)}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
);
};

LanguageSelector.propTypes = {
langCookieName: PropTypes.string.isRequired,
options: PropTypes.arrayOf(PropTypes.string).isRequired,
username: PropTypes.string,
};

export default injectIntl(LanguageSelector);
Loading
Loading