diff --git a/__tests__/integration/mirador/language_switching.test.js b/__tests__/integration/mirador/language_switching.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..134ec4a3b57f4e060f6f2f87e1da7a452e48e5eb
--- /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/App.test.js b/__tests__/src/components/App.test.js
index 0a84c0f4545125de88f3dc692967d122758ecd91..2a246c7a47d8116f84e6a8436968ebbcd6123fc6 100644
--- a/__tests__/src/components/App.test.js
+++ b/__tests__/src/components/App.test.js
@@ -13,6 +13,7 @@ import i18n from '../../../src/i18n';
 function createWrapper(props) {
   return shallow(
     <App
+      language="en"
       isFullscreenEnabled={false}
       isWorkspaceControlPanelVisible
       setWorkspaceFullscreen={() => {}}
@@ -80,4 +81,14 @@ describe('App', () => {
     expect(wrapper.find(Workspace).length).toBe(0);
     expect(wrapper.find(WorkspaceAdd).length).toBe(1);
   });
+
+  describe('componentDidUpdate()', () => {
+    it('changes the i18n language if the language prop has been updated', () => {
+      const wrapper = createWrapper();
+
+      expect(i18n.language).toEqual('en');
+      wrapper.setProps({ language: 'de' });
+      expect(i18n.language).toEqual('de');
+    });
+  });
 });
diff --git a/__tests__/src/components/LanguageSettings.test.js b/__tests__/src/components/LanguageSettings.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ae798ec89479cf348e61fa0a293d09b6ff7cd20
--- /dev/null
+++ b/__tests__/src/components/LanguageSettings.test.js
@@ -0,0 +1,84 @@
+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
+      handleClick={() => {}}
+      languages={{}}
+      {...props}
+    />,
+  );
+}
+
+describe('LanguageSettings', () => {
+  let wrapper;
+  const languages = [
+    { locale: 'de', label: 'Deutsch', current: true },
+    { locale: 'en', label: 'English', current: false },
+  ];
+
+
+  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');
+  });
+});
diff --git a/__tests__/src/components/NestedMenu.test.js b/__tests__/src/components/NestedMenu.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..422b947ecb2618fba15b951a88c75dce8ee3d18e
--- /dev/null
+++ b/__tests__/src/components/NestedMenu.test.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import NestedMenu from '../../../src/components/NestedMenu';
+
+/**
+ * Helper function to wrap creating a NestedMenu component
+*/
+function createWrapper(props) {
+  return shallow(
+    <NestedMenu
+      icon={<>GivenIcon</>}
+      label="GivenLabel"
+      {...props}
+    >
+      <>GivenChildren</>
+    </NestedMenu>,
+  );
+}
+
+describe('NestedMenu', () => {
+  let wrapper;
+
+  it('renders the given icon wrapped in a MUI ListItemIcon', () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.find('WithStyles(ListItemIcon)').children().text()).toEqual('GivenIcon');
+  });
+
+  it('renders the given label wrapped in a MUI Typography', () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.find('WithStyles(Typography)').children().text()).toEqual('GivenLabel');
+  });
+
+  it('renders the given children wrapped in a MUI Collapse', () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.find('WithStyles(Collapse)').children().text()).toEqual('GivenChildren');
+  });
+
+  it('toggles the local nestedMenuIsOpen state when clicking the MenuItem', () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.state().nestedMenuIsOpen).toBe(false);
+    wrapper.find('WithStyles(MenuItem)').simulate('click');
+    expect(wrapper.state().nestedMenuIsOpen).toBe(true);
+    wrapper.find('WithStyles(MenuItem)').simulate('click');
+    expect(wrapper.state().nestedMenuIsOpen).toBe(false);
+  });
+
+  it('renders the appropriate expand/collapse icon based on the menu open state', () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.state().nestedMenuIsOpen).toBe(false);
+    expect(wrapper.find('pure(ExpandMoreSharpIcon)').length).toBe(1);
+    expect(wrapper.find('purl(ExpandLessSharpIcon)').length).toBe(0);
+    wrapper.setState({ nestedMenuIsOpen: true });
+    expect(wrapper.find('pure(ExpandMoreSharpIcon)').length).toBe(0);
+    expect(wrapper.find('pure(ExpandLessSharpIcon)').length).toBe(1);
+  });
+
+  it("the MUI Collapse component's in prop is based on the nestedMenuIsOpen state", () => {
+    wrapper = createWrapper();
+
+    expect(wrapper.state().nestedMenuIsOpen).toBe(false);
+    expect(wrapper.find('WithStyles(Collapse)').find({ in: false }).length).toBe(1);
+    expect(wrapper.find('WithStyles(Collapse)').find({ in: true }).length).toBe(0);
+
+    wrapper.setState({ nestedMenuIsOpen: true });
+    expect(wrapper.find('WithStyles(Collapse)').find({ in: true }).length).toBe(1);
+    expect(wrapper.find('WithStyles(Collapse)').find({ in: false }).length).toBe(0);
+  });
+});
diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js
index f3f04a75fe416ca6898e2b79dcceae4866b885fd..34e3abb4d77ae7a4bad28a1720936133bdf33b61 100644
--- a/__tests__/src/selectors/index.test.js
+++ b/__tests__/src/selectors/index.test.js
@@ -7,6 +7,7 @@ import {
   getCanvasLabel,
   getCompanionWindowForPosition,
   getDestructuredMetadata,
+  getLanguagesFromConfigWithCurrent,
   getSelectedCanvas,
   getWindowManifest,
   getManifestLogo,
@@ -347,3 +348,18 @@ describe('getCompanionWindowForPosition', () => {
     expect(received).toBeUndefined();
   });
 });
+
+describe('getLanguagesFromConfigWithCurrent', () => {
+  it('returns an array of objects with locale, label, and current properties', () => {
+    const state = {
+      config: { language: 'epo', availableLanguages: { epo: 'Esparanto', tlh: 'Klingon' } },
+    };
+
+    const expected = [
+      { locale: 'epo', label: 'Esparanto', current: true },
+      { locale: 'tlh', label: 'Klingon', current: false },
+    ];
+
+    expect(getLanguagesFromConfigWithCurrent(state)).toEqual(expected);
+  });
+});
diff --git a/locales/de/translation.json b/locales/de/translation.json
index 43f71407ffa4c1cd4a163f85d41ba5d193ddb668..48e789a857c4ada389ed5c596513cfb618828063 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:",
@@ -45,8 +46,6 @@
     "zoomReset": "Ansicht zurücksetzen",
     "hideZoomControls": "Zoom-Steuerung verbergen",
     "showZoomControls": "Zoom-Steuerung anzeigen",
-    "numItems": "{{number}} Elemente",
-
-
+    "numItems": "{{number}} Elemente"
   }
 }
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 8dd85ecce50b36df10f904f7beb8adeca293366e..5320a8eaa8ba579b08632f6e8930d74cdb187b2b 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/App.js b/src/components/App.js
index cee00e4bc969f20504dce767615e67935a112927..55bb715b5d4aafb9b5fe21425f75cddc50579792 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -15,6 +15,33 @@ import i18n from '../i18n';
  * @prop {Object} manifests
  */
 class App extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.i18n = i18n;
+  }
+
+  /**
+   * Set i18n language on component mount
+   */
+  componentDidMount() {
+    const { language } = this.props;
+
+    this.i18n.changeLanguage(language);
+  }
+
+  /**
+   * Update the i18n language if it is changed
+   */
+  componentDidUpdate(prevProps) {
+    const { language } = this.props;
+
+    if (prevProps.language !== language) {
+      this.i18n.changeLanguage(language);
+    }
+  }
+
   /**
    * render
    * @return {String} - HTML markup for the component
@@ -26,12 +53,12 @@ class App extends Component {
     } = this.props;
 
     Object.keys(translations).forEach((lng) => {
-      i18n.addResourceBundle(lng, 'translation', translations[lng], true, true);
+      this.i18n.addResourceBundle(lng, 'translation', translations[lng], true, true);
     });
 
     return (
       <div className={classNames(classes.background, ns('app'))}>
-        <I18nextProvider i18n={i18n}>
+        <I18nextProvider i18n={this.i18n}>
           <MuiThemeProvider theme={createMuiTheme(theme)}>
             <Fullscreen
               enabled={isFullscreenEnabled}
@@ -55,6 +82,7 @@ class App extends Component {
 }
 
 App.propTypes = {
+  language: PropTypes.string.isRequired,
   theme: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   translations: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   isFullscreenEnabled: PropTypes.bool,
diff --git a/src/components/LanguageSettings.js b/src/components/LanguageSettings.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebe77d427f6a8803fec3b5e66a52635d4c034560
--- /dev/null
+++ b/src/components/LanguageSettings.js
@@ -0,0 +1,58 @@
+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,
+    } = this.props;
+
+    return (
+      <List>
+        {
+          languages.map(language => (
+            <MenuItem
+              button={!language.current}
+              key={language.locale}
+              onClick={() => { handleClick(language.locale); }}
+            >
+              {
+                language.current
+                  && <ListItemIcon><CheckIcon /></ListItemIcon>
+              }
+              <ListItemText inset>
+                <Typography variant="inherit">
+                  {language.label}
+                </Typography>
+              </ListItemText>
+            </MenuItem>
+          ))
+        }
+      </List>
+    );
+  }
+}
+
+LanguageSettings.propTypes = {
+  handleClick: PropTypes.func.isRequired,
+  languages: PropTypes.arrayOf(
+    PropTypes.shape({
+      current: PropTypes.bool.isRequired,
+      label: PropTypes.string.isRequired,
+      locale: PropTypes.string.isRequired,
+    }),
+  ).isRequired,
+};
diff --git a/src/components/NestedMenu.js b/src/components/NestedMenu.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ced10fde4ff8c247a0746ee8b00d105599fff46
--- /dev/null
+++ b/src/components/NestedMenu.js
@@ -0,0 +1,72 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Collapse from '@material-ui/core/Collapse';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import MenuItem from '@material-ui/core/MenuItem';
+import Typography from '@material-ui/core/Typography';
+import ExpandLess from '@material-ui/icons/ExpandLessSharp';
+import ExpandMore from '@material-ui/icons/ExpandMoreSharp';
+
+/**
+ * NestedMenu ~ A presentation component to render a menu item and have
+ * it control the visibility of the MUI List passed in as the children
+*/
+export default class NestedMenu extends Component {
+  /**
+   * constructor -
+   */
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      nestedMenuIsOpen: false,
+    };
+
+    this.handleMenuClick = this.handleMenuClick.bind(this);
+  }
+
+  /**
+   * handleMenuClick toggles the nestedMenuIsOpen state
+   */
+  handleMenuClick() {
+    const { nestedMenuIsOpen } = this.state;
+
+    this.setState({
+      nestedMenuIsOpen: !nestedMenuIsOpen,
+    });
+  }
+
+  /**
+   * Returns the rendered component
+  */
+  render() {
+    const { nestedMenuIsOpen } = this.state;
+    const { children, icon, label } = this.props;
+    return (
+      <>
+        <MenuItem onClick={this.handleMenuClick}>
+          <ListItemIcon>{icon}</ListItemIcon>
+          {/* ListItemText adds left padding and we want this to line-up with menu items */}
+          <ListItemText style={{ paddingLeft: 0 }}>
+            <Typography varient="inherit">{label}</Typography>
+          </ListItemText>
+          {
+            nestedMenuIsOpen
+              ? <ExpandLess />
+              : <ExpandMore />
+          }
+        </MenuItem>
+        <Collapse in={nestedMenuIsOpen} timeout="auto" unmountOnExit>
+          {children}
+        </Collapse>
+      </>
+    );
+  }
+}
+
+NestedMenu.propTypes = {
+  children: PropTypes.element.isRequired,
+  icon: PropTypes.element.isRequired,
+  label: PropTypes.string.isRequired,
+};
diff --git a/src/components/WorkspaceMenu.js b/src/components/WorkspaceMenu.js
index de769d15a8923841ed7fce8a2495509423718f22..facbe4fbd247422b43d7a8d88acfed61df7f3456 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 df3444143db0c59f5de76cc5ebd4de0583ce6b24..f0bf81a995ccec574b25f759897917a37a897b43 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -10,6 +10,11 @@ export default {
       useNextVariants: true // set so that console deprecation warning is removed
     }
   },
+  language: 'en',
+  availableLanguages: { // All the languages available in the language switcher
+    de: 'Deutsch',
+    en: 'English',
+  },
   translations: {
   },
   window: {
diff --git a/src/containers/App.js b/src/containers/App.js
index 87ebc50d90b2ab0009497f471251a179a086081b..8136706c3b6ca75be63d7fd6468645a351e12abf 100644
--- a/src/containers/App.js
+++ b/src/containers/App.js
@@ -11,6 +11,7 @@ import App from '../components/App';
  */
 const mapStateToProps = state => (
   {
+    language: state.config.language,
     theme: state.config.theme,
     translations: state.config.translations,
     isFullscreenEnabled: state.workspace.isFullscreenEnabled,
diff --git a/src/containers/LanguageSettings.js b/src/containers/LanguageSettings.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffd9c2147e607ed7aa7cc45a69a43ac734d4ea7d
--- /dev/null
+++ b/src/containers/LanguageSettings.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import * as actions from '../state/actions';
+import { getLanguagesFromConfigWithCurrent } from '../state/selectors';
+import LanguageSettings from '../components/LanguageSettings';
+
+/**
+ * Map state to props for connect
+ */
+const mapStateToProps = state => ({
+  languages: getLanguagesFromConfigWithCurrent(state),
+});
+
+/**
+ * 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);
diff --git a/src/i18n.js b/src/i18n.js
index dac6958f05709d746f951f1f33fb68cbc2f62a6f..e6d0d2daa855e9a950205aa8474c66fb67662b72 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1,9 +1,12 @@
 import i18n from 'i18next';
 import { reactI18nextModule } from 'react-i18next';
+import de from '../locales/de/translation.json';
 import en from '../locales/en/translation.json';
 
+
 // Load translations for each language
 const resources = {
+  de,
   en,
 };
 
diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js
index e7b4565c634581f94ddc3ff0caf2e9f251c2dacd..adcaee87f3dff77bac6c604cbc24f7c4896c58d7 100644
--- a/src/state/selectors/index.js
+++ b/src/state/selectors/index.js
@@ -201,3 +201,18 @@ export function getCompanionWindowForPosition(state, windowId, position) {
 export function getCompantionWindowIds(state, windowId) {
   return state.windows[windowId].companionWindowIds;
 }
+
+/**
+* Return languages from config (in state) and indicate which is currently set
+* @param {object} state
+* @return {Array} [ {locale: 'de', label: 'Deutsch', current: true}, ... ]
+*/
+export function getLanguagesFromConfigWithCurrent(state) {
+  const { availableLanguages, language } = state.config;
+
+  return Object.keys(availableLanguages).map(key => ({
+    locale: key,
+    label: availableLanguages[key],
+    current: key === language,
+  }));
+}