From 991c944ac3f646c9ef18e7cd9802524d20446ee7 Mon Sep 17 00:00:00 2001
From: Jessie Keck <jessie.keck@gmail.com>
Date: Thu, 21 Feb 2019 15:04:49 -0800
Subject: [PATCH] Add a LanguageSettings component and hook it up to the
 WorkspaceMenu.

---
 .../mirador/language_switching.test.js        | 24 +++++
 .../src/components/LanguageSettings.test.js   | 92 +++++++++++++++++++
 locales/de/translation.json                   |  1 +
 locales/en/translation.json                   |  1 +
 src/components/LanguageSettings.js            | 53 +++++++++++
 src/components/WorkspaceMenu.js               |  9 ++
 src/config/settings.js                        |  4 +
 src/containers/LanguageSettings.js            | 25 +++++
 8 files changed, 209 insertions(+)
 create mode 100644 __tests__/integration/mirador/language_switching.test.js
 create mode 100644 __tests__/src/components/LanguageSettings.test.js
 create mode 100644 src/components/LanguageSettings.js
 create mode 100644 src/containers/LanguageSettings.js

diff --git a/__tests__/integration/mirador/language_switching.test.js b/__tests__/integration/mirador/language_switching.test.js
new file mode 100644
index 000000000..134ec4a3b
--- /dev/null
+++ b/__tests__/integration/mirador/language_switching.test.js
@@ -0,0 +1,24 @@
+describe('Language Switching', () => {
+  describe('Application Language', () => {
+    it('allows the user to switch the application language', async () => {
+      await page.goto('http://127.0.0.1:4488/__tests__/integration/mirador/');
+
+      await expect(page).toClick('#menuBtn');
+      await expect(page).toMatchElement('ul[role="menu"]');
+      await expect(page).toMatchElement('li p', { text: 'Language' });
+
+      await expect(page).not.toMatchElement('li', { text: 'Deutsch' });
+      await expect(page).not.toMatchElement('li', { text: 'English' });
+      await expect(page).toClick('li', { text: 'Language' });
+      await expect(page).toMatchElement('li', { text: 'Deutsch' });
+      await expect(page).toMatchElement('li', { text: 'English' });
+
+      await expect(page).toMatchElement('[aria-label="Toggle window sidebar"]');
+      await expect(page).not.toMatchElement('[aria-label="Seitenleiste umschalten"]');
+      await expect(page).toClick('li', { text: 'Deutsch' });
+      await page.waitFor(1000);
+      await expect(page).not.toMatchElement('[aria-label="Toggle window sidebar"]');
+      await expect(page).toMatchElement('[aria-label="Seitenleiste umschalten"]');
+    });
+  });
+});
diff --git a/__tests__/src/components/LanguageSettings.test.js b/__tests__/src/components/LanguageSettings.test.js
new file mode 100644
index 000000000..e38cbb8cd
--- /dev/null
+++ b/__tests__/src/components/LanguageSettings.test.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import LanguageSettings from '../../../src/components/LanguageSettings';
+
+/**
+ * Helper function to create a shallow wrapper around LanguageSettings
+ */
+function createWrapper(props) {
+  return shallow(
+    <LanguageSettings
+      active={lang => lang === 'de'}
+      handleClick={() => {}}
+      languages={{}}
+      {...props}
+    />,
+  );
+}
+
+describe('LanguageSettings', () => {
+  let wrapper;
+  const languages = {
+    de: 'Deutsch',
+    en: 'English',
+  };
+
+
+  it('renders a list with a list item for each language passed in props', () => {
+    wrapper = createWrapper({ languages });
+
+    expect(wrapper.find('WithStyles(MenuItem)').length).toBe(2);
+  });
+
+  it('non-active list items are buttons (and active are not)', () => {
+    wrapper = createWrapper({ languages });
+
+    expect(
+      wrapper
+        .find('WithStyles(MenuItem)')
+        .first() // The German / active button
+        .prop('button'),
+    ).toBe(false);
+
+    expect(
+      wrapper
+        .find('WithStyles(MenuItem)')
+        .last() // The English / non-active button
+        .prop('button'),
+    ).toBe(true);
+  });
+
+  it('renders the check icon when the active prop returns true', () => {
+    wrapper = createWrapper({ languages });
+
+    expect(
+      wrapper
+        .find('WithStyles(MenuItem)')
+        .first()
+        .find('WithStyles(ListItemIcon) pure(CheckSharpIcon)')
+        .length,
+    ).toBe(1);
+  });
+
+  it('renders the language value in an Typography element wrapped in a ListItemText', () => {
+    wrapper = createWrapper({ languages });
+
+    const firstListText = wrapper
+      .find('WithStyles(MenuItem)')
+      .first()
+      .find('WithStyles(ListItemText) WithStyles(Typography)')
+      .children()
+      .text();
+
+    expect(firstListText).toEqual('Deutsch');
+  });
+
+  it('triggers the handleClick prop when clicking a list item', () => {
+    const mockHandleClick = jest.fn();
+    wrapper = createWrapper({ languages, handleClick: mockHandleClick });
+
+    wrapper.find('WithStyles(MenuItem)').last().simulate('click');
+
+    expect(mockHandleClick).toHaveBeenCalledTimes(1);
+    expect(mockHandleClick).toHaveBeenCalledWith('en');
+  });
+
+  it('passes the language prop to the active prop function to determine if the given language is active', () => {
+    const mockActiveFn = jest.fn();
+    wrapper = createWrapper({ active: mockActiveFn, languages: { en: 'English' } });
+
+    expect(mockActiveFn).toHaveBeenCalledWith('en');
+  });
+});
diff --git a/locales/de/translation.json b/locales/de/translation.json
index 448fab989..48e789a85 100644
--- a/locales/de/translation.json
+++ b/locales/de/translation.json
@@ -21,6 +21,7 @@
     "downloadExportWorkspace": "Download/Export Arbeitsfläche",
     "fetchManifest": "Hinzufügen",
     "fullScreen": "Vollbild",
+    "language": "Sprache",
     "light": "Hell",
     "listAllOpenWindows": "Liste der geöffneten Fenster",
     "manifestError": "Die Ressource konnte nicht hinzugefügt werden:",
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 8dd85ecce..5320a8eaa 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -22,6 +22,7 @@
     "fetchManifest": "Add",
     "fullScreen": "Full Screen",
     "hideZoomControls": "Hide zoom controls",
+    "language": "Language",
     "light": "Light",
     "listAllOpenWindows": "List all open windows",
     "manifestError": "The resource cannot be added:",
diff --git a/src/components/LanguageSettings.js b/src/components/LanguageSettings.js
new file mode 100644
index 000000000..bdfbe1348
--- /dev/null
+++ b/src/components/LanguageSettings.js
@@ -0,0 +1,53 @@
+import React, { Component } from 'react';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import List from '@material-ui/core/List';
+import MenuItem from '@material-ui/core/MenuItem';
+import Typography from '@material-ui/core/Typography';
+import CheckIcon from '@material-ui/icons/CheckSharp';
+import PropTypes from 'prop-types';
+
+/**
+ * LanguageSettings ~ the workspace sub menu to change the language
+ * of the application
+*/
+export default class LanguageSettings extends Component {
+  /**
+   * Returns the rendered component
+  */
+  render() {
+    const {
+      handleClick, languages, active,
+    } = this.props;
+
+    return (
+      <List>
+        {
+          Object.keys(languages).map(language => (
+            <MenuItem
+              button={!(active(language))}
+              key={language}
+              onClick={() => { handleClick(language); }}
+            >
+              {
+                active(language)
+                  && <ListItemIcon><CheckIcon /></ListItemIcon>
+              }
+              <ListItemText inset>
+                <Typography variant="inherit">
+                  {languages[language]}
+                </Typography>
+              </ListItemText>
+            </MenuItem>
+          ))
+        }
+      </List>
+    );
+  }
+}
+
+LanguageSettings.propTypes = {
+  active: PropTypes.func.isRequired,
+  handleClick: PropTypes.func.isRequired,
+  languages: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+};
diff --git a/src/components/WorkspaceMenu.js b/src/components/WorkspaceMenu.js
index de769d15a..facbe4fbd 100644
--- a/src/components/WorkspaceMenu.js
+++ b/src/components/WorkspaceMenu.js
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import Menu from '@material-ui/core/Menu';
 import Divider from '@material-ui/core/Divider';
+import LanguageIcon from '@material-ui/icons/Language';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import LoupeIcon from '@material-ui/icons/Loupe';
 import MenuItem from '@material-ui/core/MenuItem';
@@ -9,6 +10,8 @@ import SaveAltIcon from '@material-ui/icons/SaveAlt';
 import SettingsIcon from '@material-ui/icons/Settings';
 import ViewHeadlineIcon from '@material-ui/icons/ViewHeadline';
 import PropTypes from 'prop-types';
+import LanguageSettings from '../containers/LanguageSettings';
+import NestedMenu from './NestedMenu';
 import WindowList from '../containers/WindowList';
 import WorkspaceSettings from '../containers/WorkspaceSettings';
 import WorkspaceExport from '../containers/WorkspaceExport';
@@ -92,6 +95,7 @@ class WorkspaceMenu extends Component {
             </ListItemIcon>
             <Typography varient="inherit">{t('listAllOpenWindows')}</Typography>
           </MenuItem>
+          <Divider />
           <MenuItem
             aria-haspopup="true"
             onClick={(e) => { this.handleZoomToggleClick(e); handleClose(e); }}
@@ -104,6 +108,11 @@ class WorkspaceMenu extends Component {
               { showZoomControls ? t('hideZoomControls') : t('showZoomControls') }
             </Typography>
           </MenuItem>
+
+          <NestedMenu icon={<LanguageIcon />} label={t('language')}>
+            <LanguageSettings afterSelect={handleClose} />
+          </NestedMenu>
+
           <Divider />
           <MenuItem
             aria-haspopup="true"
diff --git a/src/config/settings.js b/src/config/settings.js
index a2d481e5d..f0bf81a99 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -11,6 +11,10 @@ export default {
     }
   },
   language: 'en',
+  availableLanguages: { // All the languages available in the language switcher
+    de: 'Deutsch',
+    en: 'English',
+  },
   translations: {
   },
   window: {
diff --git a/src/containers/LanguageSettings.js b/src/containers/LanguageSettings.js
new file mode 100644
index 000000000..b3d543858
--- /dev/null
+++ b/src/containers/LanguageSettings.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import * as actions from '../state/actions';
+import LanguageSettings from '../components/LanguageSettings';
+
+/**
+ * Map state to props for connect
+ */
+const mapStateToProps = state => ({
+  languages: state.config.availableLanguages,
+  currentLanguage: state.config.language,
+  active: language => language === state.config.language,
+});
+
+/**
+ * Map action dispatches to props for connect
+ */
+const mapDispatchToProps = (dispatch, { afterSelect }) => ({
+  handleClick: (language) => {
+    dispatch(actions.updateConfig({ language }));
+
+    afterSelect && afterSelect();
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(LanguageSettings);
-- 
GitLab