fix(client): set the correct language to the HTML lang attribute (#42729)
This commit is contained in:
52
client/src/components/app-mount-notifier.test.tsx
Normal file
52
client/src/components/app-mount-notifier.test.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { i18nextCodes } from '../../../config/i18n/all-langs';
|
||||||
|
import AppMountNotifier from './app-mount-notifier';
|
||||||
|
import { createStore } from '../redux/createStore';
|
||||||
|
import i18nTestConfig from '../../i18n/configForTests';
|
||||||
|
|
||||||
|
jest.mock('react-ga');
|
||||||
|
jest.unmock('react-i18next');
|
||||||
|
|
||||||
|
type Language = [string, string];
|
||||||
|
|
||||||
|
const store = createStore();
|
||||||
|
|
||||||
|
// Create a nested array for languages
|
||||||
|
const languages = Object.keys(i18nextCodes).map(
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return */
|
||||||
|
// @ts-ignore
|
||||||
|
// TODO: convert `all-langs.js` to TypeScript
|
||||||
|
(key): Language => [i18nextCodes[key], key]
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('AppMountNotifier', () => {
|
||||||
|
const setup = (lang: string) => {
|
||||||
|
i18nTestConfig.language = lang;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18nTestConfig}>
|
||||||
|
<AppMountNotifier render={() => <p>App content</p>} />
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.each(languages)(
|
||||||
|
'should set the lang attribute to %s if the language is %s',
|
||||||
|
async langCode => {
|
||||||
|
setup(langCode);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector('html')).toHaveAttribute(
|
||||||
|
'lang',
|
||||||
|
langCode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
@ -1,27 +0,0 @@
|
|||||||
import { Component, ReactNode } from 'react';
|
|
||||||
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { appMount } from '../redux';
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
|
|
||||||
bindActionCreators({ appMount }, dispatch);
|
|
||||||
|
|
||||||
type AppMountNotifierProps = {
|
|
||||||
appMount: () => void;
|
|
||||||
render: () => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
class AppMountNotifier extends Component<AppMountNotifierProps> {
|
|
||||||
static displayName = 'AppMountNotifier';
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
return this.props.appMount();
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return this.props.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AppMountNotifier);
|
|
38
client/src/components/app-mount-notifier.tsx
Normal file
38
client/src/components/app-mount-notifier.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { appMount } from '../redux';
|
||||||
|
|
||||||
|
interface AppMountNotifierProps {
|
||||||
|
render: () => React.ReactNode;
|
||||||
|
appMount: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
|
bindActionCreators({ appMount }, dispatch);
|
||||||
|
|
||||||
|
const AppMountNotifier = ({
|
||||||
|
render,
|
||||||
|
appMount
|
||||||
|
}: AppMountNotifierProps): JSX.Element => {
|
||||||
|
useEffect(() => {
|
||||||
|
appMount();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet htmlAttributes={{ lang: i18n.language }} />
|
||||||
|
{render()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AppMountNotifier.displayName = 'AppMountNotifier';
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(AppMountNotifier);
|
@ -10,7 +10,6 @@ module.exports = {
|
|||||||
'<rootDir>/client/src/__mocks__/styleMock.js',
|
'<rootDir>/client/src/__mocks__/styleMock.js',
|
||||||
// CSS Modules - match files that end with 'module.css'
|
// CSS Modules - match files that end with 'module.css'
|
||||||
'\\.module\\.css$': 'identity-obj-proxy',
|
'\\.module\\.css$': 'identity-obj-proxy',
|
||||||
'react-i18next': '<rootDir>/client/src/__mocks__/react-i18nextMock.js',
|
|
||||||
'^lodash-es$': 'lodash'
|
'^lodash-es$': 'lodash'
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
|
Reference in New Issue
Block a user