Skip to content

Commit 82bfc97

Browse files
committed
refactor: change lang selector logic
1 parent 47d4677 commit 82bfc97

File tree

9 files changed

+190
-136
lines changed

9 files changed

+190
-136
lines changed

README.rst

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,19 @@ This library has the following exports:
8686
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
8787
* ``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.
8888

89-
<Footer /> component props
90-
==========================
89+
Language Selector
90+
-----------------
9191

92-
* onLanguageSelected: Provides the footer with an event handler for when the user selects a
93-
language from its dropdown.
94-
* supportedLanguages: An array of objects representing available languages. See example below for object shape.
92+
The language selector dropdown is optional and can be enabled by setting the MFE configuration variable ``ENABLE_FOOTER_LANG_SELECTOR`` to ``true``.
93+
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.
94+
Example:
95+
96+
.. code-block:: python
97+
98+
MFE_CONFIG["EDX_FRONTEND_APP_CONFIG"] = {
99+
"ENABLE_FOOTER_LANG_SELECTOR": True,
100+
"SITE_SUPPORTED_LANGUAGES": ['en', 'es', 'fr', 'pt-br'],
101+
}
95102
96103
Plugin
97104
======
@@ -108,13 +115,7 @@ Component Usage Example::
108115

109116
...
110117

111-
<Footer
112-
onLanguageSelected={(languageCode) => {/* set language */}}
113-
supportedLanguages={[
114-
{ label: 'English', value: 'en'},
115-
{ label: 'Español', value: 'es' },
116-
]}
117-
/>
118+
<Footer />
118119

119120
* `An example of minimal component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L23>`_
120121
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L9>`_

env.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is used only for the example application.
2+
const config = {
3+
ENABLE_FOOTER_LANG_SELECTOR: true,
4+
SITE_SUPPORTED_LANGUAGES: ['es', 'en'],
5+
};
6+
7+
export default config;

example/index.jsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@ subscribe(APP_READY, () => {
1515
authenticatedUser: null,
1616
config: getConfig(),
1717
}}>
18-
<Footer
19-
onLanguageSelected={() => {}}
20-
supportedLanguages={[
21-
{ label: 'English', value: 'en' },
22-
{ label: 'Español', value: 'es' },
23-
]}
24-
/>
18+
<Footer />
2519
</AppContext.Provider>
2620
</AppProvider>,
2721
document.getElementById('root'),

src/components/Footer.jsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,10 @@ class SiteFooter extends React.Component {
3535

3636
render() {
3737
const {
38-
supportedLanguages,
39-
onLanguageSelected,
4038
logo,
4139
intl,
4240
} = this.props;
43-
const showLanguageSelector = supportedLanguages.length > 0 && onLanguageSelected;
44-
const { config } = this.context;
45-
41+
const { config, authenticatedUser } = this.context;
4642
return (
4743
<footer
4844
role="contentinfo"
@@ -61,11 +57,14 @@ class SiteFooter extends React.Component {
6157
/>
6258
</a>
6359
<div className="flex-grow-1" />
64-
{showLanguageSelector && (
65-
<LanguageSelector
66-
options={supportedLanguages}
67-
onSubmit={onLanguageSelected}
68-
/>
60+
{config.ENABLE_FOOTER_LANG_SELECTOR && (
61+
<div className="mb-2">
62+
<LanguageSelector
63+
options={config.SITE_SUPPORTED_LANGUAGES}
64+
username={authenticatedUser?.username}
65+
langCookieName={config.LANGUAGE_PREFERENCE_COOKIE_NAME}
66+
/>
67+
</div>
6968
)}
7069
</div>
7170
</footer>
@@ -78,17 +77,10 @@ SiteFooter.contextType = AppContext;
7877
SiteFooter.propTypes = {
7978
intl: intlShape.isRequired,
8079
logo: PropTypes.string,
81-
onLanguageSelected: PropTypes.func,
82-
supportedLanguages: PropTypes.arrayOf(PropTypes.shape({
83-
label: PropTypes.string.isRequired,
84-
value: PropTypes.string.isRequired,
85-
})),
8680
};
8781

8882
SiteFooter.defaultProps = {
8983
logo: undefined,
90-
onLanguageSelected: undefined,
91-
supportedLanguages: [],
9284
};
9385

9486
export default injectIntl(SiteFooter);

src/components/Footer.test.jsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { render, screen } from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
66
import { IntlProvider } from '@edx/frontend-platform/i18n';
77
import { AppContext } from '@edx/frontend-platform/react';
8+
import '@testing-library/jest-dom';
89

910
import Footer from './Footer';
1011
import FooterSlot from '../plugin-slots/FooterSlot';
@@ -30,33 +31,36 @@ const FooterWithContext = ({ locale = 'es' }) => {
3031
);
3132
};
3233

33-
const FooterWithLanguageSelector = ({ languageSelected = () => {} }) => {
34+
const { LANGUAGE_PREFERENCE_COOKIE_NAME } = process.env;
35+
const FooterWithLanguageSelector = ({ authenticatedUser = null }) => {
3436
const contextValue = useMemo(() => ({
35-
authenticatedUser: null,
37+
authenticatedUser,
3638
config: {
39+
ENABLE_FOOTER_LANG_SELECTOR: true,
40+
LANGUAGE_PREFERENCE_COOKIE_NAME,
3741
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
3842
LMS_BASE_URL: process.env.LMS_BASE_URL,
43+
SITE_SUPPORTED_LANGUAGES: ['es', 'en'],
3944
},
40-
}), []);
45+
}), [authenticatedUser]);
4146

4247
return (
4348
<IntlProvider locale="en">
4449
<AppContext.Provider
4550
value={contextValue}
4651
>
47-
<Footer
48-
onLanguageSelected={languageSelected}
49-
supportedLanguages={[
50-
{ label: 'English', value: 'en' },
51-
{ label: 'Español', value: 'es' },
52-
]}
53-
/>
52+
<Footer />
5453
</AppContext.Provider>
5554
</IntlProvider>
5655
);
5756
};
5857

5958
describe('<Footer />', () => {
59+
beforeEach(() => {
60+
jest.clearAllMocks();
61+
initializeMockApp();
62+
});
63+
6064
describe('renders correctly', () => {
6165
it('renders without a language selector', () => {
6266
const tree = renderer

src/components/LanguageSelector.jsx

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getConfig } from '@edx/frontend-platform';
2+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
3+
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
4+
5+
export async function patchPreferences(username, params) {
6+
let processedParams = snakeCaseObject(params);
7+
processedParams = convertKeyNames(processedParams, {
8+
pref_lang: 'pref-lang',
9+
});
10+
11+
await getAuthenticatedHttpClient()
12+
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
13+
headers: { 'Content-Type': 'application/merge-patch+json' },
14+
});
15+
16+
return params;
17+
}
18+
19+
export async function postSetLang(code) {
20+
const formData = new FormData();
21+
const requestConfig = {
22+
headers: {
23+
Accept: 'application/json',
24+
'X-Requested-With': 'XMLHttpRequest',
25+
},
26+
};
27+
const url = `${getConfig().LMS_BASE_URL}/i18n/setlang/`;
28+
formData.append('language', code);
29+
30+
await getAuthenticatedHttpClient()
31+
.post(url, formData, requestConfig);
32+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useMemo, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { publish } from '@edx/frontend-platform';
4+
import {
5+
injectIntl, LOCALE_CHANGED, getLocale, handleRtl, getPrimaryLanguageSubtag,
6+
} from '@edx/frontend-platform/i18n';
7+
import { logError } from '@edx/frontend-platform/logging';
8+
import { Dropdown, useWindowSize } from '@openedx/paragon';
9+
import { Language } from '@openedx/paragon/icons';
10+
import { getCookies } from '@edx/frontend-platform/i18n/lib';
11+
import { patchPreferences, postSetLang } from './data';
12+
13+
const onLanguageSelected = async ({ langCookieName, username, selectedLlocale }) => {
14+
try {
15+
if (username) {
16+
await patchPreferences(username, { prefLang: selectedLlocale });
17+
await postSetLang(selectedLlocale);
18+
} else {
19+
getCookies().set(langCookieName, selectedLlocale);
20+
}
21+
publish(LOCALE_CHANGED, getLocale());
22+
handleRtl();
23+
} catch (error) {
24+
logError(error);
25+
}
26+
};
27+
const getLocaleName = (locale) => {
28+
const langName = new Intl.DisplayNames([locale], { type: 'language', languageDisplay: 'standard' }).of(locale);
29+
return langName.replace(/^\w/, (c) => c.toUpperCase());
30+
};
31+
32+
const LanguageSelector = ({
33+
langCookieName, options, username,
34+
}) => {
35+
const [currentLocale, setLocale] = useState(getLocale());
36+
const { width } = useWindowSize();
37+
38+
const handleSelect = (selectedLlocale) => {
39+
if (currentLocale !== selectedLlocale) {
40+
onLanguageSelected({ langCookieName, username, selectedLlocale });
41+
}
42+
setLocale(selectedLlocale);
43+
};
44+
45+
const currentLocaleLabel = useMemo(() => {
46+
if (width < 576) {
47+
return null;
48+
}
49+
if (width < 768) {
50+
return getPrimaryLanguageSubtag(currentLocale).toUpperCase();
51+
}
52+
return getLocaleName(currentLocale);
53+
}, [currentLocale, width]);
54+
55+
return (
56+
<Dropdown onSelect={handleSelect}>
57+
<Dropdown.Toggle
58+
id="lang-selector-dropdown"
59+
iconBefore={Language}
60+
variant="outline-primary"
61+
size="sm"
62+
>
63+
{currentLocaleLabel}
64+
</Dropdown.Toggle>
65+
<Dropdown.Menu>
66+
{options.map((locale) => (
67+
<Dropdown.Item key={`lang-selector-${locale}`} eventKey={locale}>
68+
{getLocaleName(locale)}
69+
</Dropdown.Item>
70+
))}
71+
</Dropdown.Menu>
72+
</Dropdown>
73+
);
74+
};
75+
76+
LanguageSelector.propTypes = {
77+
langCookieName: PropTypes.string.isRequired,
78+
options: PropTypes.arrayOf(PropTypes.string).isRequired,
79+
username: PropTypes.string,
80+
};
81+
82+
export default injectIntl(LanguageSelector);

0 commit comments

Comments
 (0)