Skip to content

Commit

Permalink
refactor: change lang selector logic
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Jan 27, 2025
1 parent bc79936 commit 1dd8994
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 151 deletions.
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
71 changes: 47 additions & 24 deletions src/components/Footer.test.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
/* 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 { fireEvent, render, waitFor } from '@testing-library/react';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getCookies } from '@edx/frontend-platform/i18n/lib';
import { AppContext } from '@edx/frontend-platform/react';
import '@testing-library/jest-dom';

import Footer from './Footer';
import { patchPreferences, postSetLang } from './LanguageSelector/data';

jest.mock('./LanguageSelector/data', () => ({
patchPreferences: jest.fn(),
postSetLang: jest.fn(),
}));

const FooterWithContext = ({ locale = 'es' }) => {
const contextValue = useMemo(() => ({
Expand All @@ -27,33 +36,36 @@ 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>
);
};

describe('<Footer />', () => {
beforeEach(() => {
jest.clearAllMocks();
initializeMockApp();
});

describe('renders correctly', () => {
it('renders without a language selector', () => {
const tree = renderer
Expand All @@ -76,21 +88,32 @@ describe('<Footer />', () => {
});

describe('handles language switching', () => {
it('calls onLanguageSelected prop when a language is changed', () => {
const mockHandleLanguageSelected = jest.fn();
render(<FooterWithLanguageSelector languageSelected={mockHandleLanguageSelected} />);
it('calls publish with LOCALE_CHANGED when the language changed', () => {
const setSpy = jest.spyOn(getCookies(), 'set');
const component = render(<FooterWithLanguageSelector />);

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

expect(mockHandleLanguageSelected).toHaveBeenCalledWith('es');
const langDropdown = component.queryByRole('button');
fireEvent.click(langDropdown);
fireEvent.click(component.queryByText('Español'));

expect(setSpy).toHaveBeenCalledWith(LANGUAGE_PREFERENCE_COOKIE_NAME, 'es');
});
it('update the lang preference for an autheticathed user', async () => {
const userData = { username: 'test-user' };
const component = render(<FooterWithLanguageSelector authenticatedUser={userData} />);

expect(component.queryByRole('button')).toBeInTheDocument();

const langDropdown = component.queryByRole('button');
fireEvent.click(langDropdown);
fireEvent.click(component.queryByText('Español'));

await waitFor(() => {
expect(patchPreferences).toHaveBeenCalledWith(userData.username, { prefLang: 'es' });
expect(postSetLang).toHaveBeenCalledWith('es');
});
});
});
});
58 changes: 0 additions & 58 deletions src/components/LanguageSelector.jsx

This file was deleted.

32 changes: 32 additions & 0 deletions src/components/LanguageSelector/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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' },
});

return params;
}

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);
}
Loading

0 comments on commit 1dd8994

Please sign in to comment.