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 10, 2025
1 parent 5879175 commit f6d340b
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint": "fedx-scripts eslint --ext .js --ext .jsx . --fix",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"start:with-theme": "paragon install-theme && npm start && npm install",
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);
}
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 null;
}
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

0 comments on commit f6d340b

Please sign in to comment.