diff --git a/.eslintrc b/.eslintrc
index 4ba53d9675b1d23768bd88ef1c480261032c41f3..35d92042471a480ef2bfb832d34104b7531dd648 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -30,5 +30,6 @@
     }],
     "react/jsx-props-no-spreading": "off",
     "arrow-parens": "off",
+    "import/no-anonymous-default-export": "off"
   }
 }
diff --git a/.gitignore b/.gitignore
index 6d95dc1313eab6b5524d10b157d243036153178e..e6ef744167cee4caf3da2acfd18c37ce8edd25b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ coverage/
 node_modules/
 package-lock.json
 *.log
+*.tgz
\ No newline at end of file
diff --git a/README.md b/README.md
index 65fe992f8829ef5ec99cbcc5af150d887547e385..fa15415b22868f246d5ef0949962b3dde6a943c8 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ https://dzkimgs.l.u-tokyo.ac.jp/videos/m3/mirador.min.js
 This project is dual-licensed under the Apache License 2.0 and the MIT license. See [LICENSE](LICENSE) for details.
 
 ---
-*NOTE: This README reflects the latest version of Mirador, Mirador 3. For previous versions, please reference that release's README directly. Latest 2.x release: [v.2.7.0](https://github.com/ProjectMirador/mirador/tree/v2.7.0)*
+⚠️ This project is for Mirador 3, the latest version of Mirador. For Mirador 2, please see [ProjectMirador/mirador2](https://github.com/projectmirador/mirador2) or legacy documentation on the [Mirador 2 wiki](https://github.com/ProjectMirador/mirador-2-wiki/wiki). Please note that the community's focus is on Mirador 3, and are unlikely to accept pull requests or provide support for Mirador 2.
 # Mirador
 ![Node.js CI](https://github.com/ProjectMirador/mirador/workflows/Node.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/ProjectMirador/mirador/branch/master/graph/badge.svg)](https://codecov.io/gh/ProjectMirador/mirador) 
 
@@ -105,6 +105,20 @@ $ npm run lint
 ```
 
 ## Debugging
-Useful browser extensions for debugging/development purposes
+
+### Local instance
+
+The following browser extensions are useful for debugging a local development instance of Mirador:
+
  - [React DevTools](https://github.com/facebook/react-devtools)
  - [Redux DevTools](https://github.com/zalmoxisus/redux-devtools-extension)
+
+### Test suite
+
+To debug the test suite, run:
+
+```sh
+$ npm run test:debug
+```
+
+then spin up a [nodejs inspector client](https://nodejs.org/en/docs/guides/debugging-getting-started/#inspector-clients) and set some breakpoints. See [here](https://www.digitalocean.com/community/tutorials/how-to-debug-node-js-with-the-built-in-debugger-and-chrome-devtools#step-3-%E2%80%94-debugging-node-js-with-chrome-devtools) for a guide to debugging with Chrome DevTools.
diff --git a/__tests__/integration/mirador/index.html b/__tests__/integration/mirador/index.html
index f40c058a38e5cce5a35c53a3cd01ed679081600a..43113f0074056a89e99fa96d4f61c093f238b3c1 100644
--- a/__tests__/integration/mirador/index.html
+++ b/__tests__/integration/mirador/index.html
@@ -38,7 +38,7 @@
          { manifestId: "https://iiif.biblissima.fr/chateauroux/B360446201_MS0005/manifest.json", provider: "Biblissima"},
          { manifestId: "https://iiif.durham.ac.uk/manifests/trifle/32150/t1/m4/q7/t1m4q77fr328/manifest", provider: "Durham University Library"},
          //{ manifestId: "https://iiif.vam.ac.uk/collections/O1023003/manifest.json", provider: "Ocean liners"},
-         { manifestId: "https://zavicajna.digitalna.rs/iiif/iiif/api/presentation/2/4aa44ad1-0b74-4590-ab09-534a38cb7c53%252F00000001%252Fostalo01%252F00000012/manifest", provider: "Библиотека 'Милутин Бојић'"},
+         { manifestId: "https://zavicajna.digitalna.rs/iiif/iiif/api/presentation/3/96571949-03d6-478e-ab44-a2d5ad68f935%252F00000001%252Fostalo01%252F00000071/manifest", provider: "Библиотека 'Милутин Бојић'"},
        ]
      });
     </script>
diff --git a/__tests__/integration/mirador/video.html b/__tests__/integration/mirador/video.html
index f43c9ed04cd593ac04afe4498cce8d7b81c495f0..8f8d77aab2e81a6bd50644a5f7bb94707eda425c 100644
--- a/__tests__/integration/mirador/video.html
+++ b/__tests__/integration/mirador/video.html
@@ -24,7 +24,7 @@
            manifestId: 'https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/manifest.json'
          },
          {
-           manifestId: 'https://iiif-commons.github.io/iiif-av-component/examples/data/iiif/lunchroom-manners.json'
+           manifestId: 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json'
          }
       ],
      });
diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js
index e7599b5d4b8f3b65aec0f6537d67f16ce662bba3..27eae582383cfe743c028569691d5105e391b6bd 100644
--- a/__tests__/src/actions/window.test.js
+++ b/__tests__/src/actions/window.test.js
@@ -45,7 +45,7 @@ describe('window actions', () => {
         ],
         elasticLayout: {
           height: 400,
-          width: 400,
+          width: 480,
           x: 260,
           y: 300,
         },
@@ -152,6 +152,31 @@ describe('window actions', () => {
       expect(action.companionWindows[0]).toMatchObject({ content: 'thumbnailNavigation' });
     });
 
+    it('enables a window to override the panel being displayed', () => {
+      const options = {
+        id: 'helloworld',
+        sideBarPanel: 'canvas',
+      };
+      const mockState = {
+        companionWindows: {},
+        config: {
+          thumbnailNavigation: {},
+          window: {
+            defaultSideBarPanel: 'info',
+          },
+        },
+        workspace: {},
+      };
+      const mockDispatch = jest.fn(() => ({}));
+      const mockGetState = jest.fn(() => mockState);
+      const thunk = actions.addWindow(options);
+
+      thunk(mockDispatch, mockGetState);
+
+      const action = mockDispatch.mock.calls[0][0];
+      expect(action.window.sideBarPanel).toEqual('canvas');
+    });
+
     it('pulls a provided manifest out', () => {
       const options = {
         canvasIndex: 1,
diff --git a/__tests__/src/components/AnnotationsOverlay.test.js b/__tests__/src/components/AnnotationsOverlay.test.js
index ce3af690541cf14b44b6c47ed9bb559bdf91adf0..83459281043dd6a8eeeca347f7fe98937a9420c7 100644
--- a/__tests__/src/components/AnnotationsOverlay.test.js
+++ b/__tests__/src/components/AnnotationsOverlay.test.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import OpenSeadragon from 'openseadragon';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { AnnotationsOverlay } from '../../../src/components/AnnotationsOverlay';
 import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay';
 import AnnotationList from '../../../src/lib/AnnotationList';
diff --git a/__tests__/src/components/AudioViewer.test.js b/__tests__/src/components/AudioViewer.test.js
index 1e807aaeeb4c5c2f4a2e7e5b748cd7b4c83ec9e7..cca67ee5dd9f4f10729c6a0a0dc955795025dadc 100644
--- a/__tests__/src/components/AudioViewer.test.js
+++ b/__tests__/src/components/AudioViewer.test.js
@@ -7,6 +7,7 @@ function createWrapper(props, suspenseFallback) {
   return shallow(
     <AudioViewer
       classes={{}}
+      audioOptions={{ crossOrigin: 'anonymous' }}
       {...props}
     />,
   );
@@ -22,8 +23,16 @@ describe('AudioViewer', () => {
           { getFormat: () => 'video/mp4', id: 2 },
         ],
       }, true);
-      expect(wrapper.contains(<source src="1" type="video/mp4" />));
-      expect(wrapper.contains(<source src="2" type="video/mp4" />));
+      expect(wrapper.contains(<source src={1} type="video/mp4" />)).toBe(true);
+      expect(wrapper.contains(<source src={2} type="video/mp4" />)).toBe(true);
+    });
+    it('passes through configurable options', () => {
+      wrapper = createWrapper({
+        audioResources: [
+          { getFormat: () => 'audio/mp3', id: 1 },
+        ],
+      }, true);
+      expect(wrapper.exists('audio[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption
     });
     it('captions', () => {
       wrapper = createWrapper({
@@ -31,12 +40,12 @@ describe('AudioViewer', () => {
           { getFormat: () => 'video/mp4', id: 1 },
         ],
         captions: [
-          { getLabel: () => 'English', getProperty: () => 'en', id: 1 },
-          { getLabel: () => 'French', getProperty: () => 'fr', id: 2 },
+          { getDefaultLabel: () => 'English', getProperty: () => 'en', id: 1 },
+          { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 },
         ],
       }, true);
-      expect(wrapper.contains(<track src="1" label="English" srcLang="en" />));
-      expect(wrapper.contains(<track src="2" label="French" srcLang="fr" />));
+      expect(wrapper.contains(<track src={1} label="English" srcLang="en" />)).toBe(true);
+      expect(wrapper.contains(<track src={2} label="French" srcLang="fr" />)).toBe(true);
     });
   });
 });
diff --git a/__tests__/src/components/CanvasAnnotations.test.js b/__tests__/src/components/CanvasAnnotations.test.js
index b68f0e367ff857dc0d02c82f1e855aab20a7c4a8..63f0674ddb3bcd05b49eb131005d812fd99ecb75 100644
--- a/__tests__/src/components/CanvasAnnotations.test.js
+++ b/__tests__/src/components/CanvasAnnotations.test.js
@@ -5,6 +5,7 @@ import Chip from '@material-ui/core/Chip';
 import MenuList from '@material-ui/core/MenuList';
 import MenuItem from '@material-ui/core/MenuItem';
 import { CanvasAnnotations } from '../../../src/components/CanvasAnnotations';
+import { ScrollTo } from '../../../src/components/ScrollTo';
 
 /** Utility function to wrap CanvasAnnotations */
 function createWrapper(props) {
@@ -62,6 +63,14 @@ describe('CanvasAnnotations', () => {
     expect(wrapper.find(MenuItem).length).toEqual(2);
   });
 
+  it('scrolls to the selected annotation', () => {
+    wrapper = createWrapper({ annotations, selectedAnnotationId: 'abc123' });
+
+    expect(wrapper.find(ScrollTo).length).toEqual(2);
+    expect(wrapper.find(ScrollTo).first().prop('scrollTo')).toEqual(true);
+    expect(wrapper.find(ScrollTo).last().prop('scrollTo')).toEqual(false);
+  });
+
   it('renders a Chip for every tag', () => {
     wrapper = createWrapper({ annotations });
 
diff --git a/__tests__/src/components/CollectionDialog.test.js b/__tests__/src/components/CollectionDialog.test.js
index 5c0a08380ec0683b65db3b1c6d597d6e0f47d257..2b057a7d100d217269cc45e1107198a0bfe828ad 100644
--- a/__tests__/src/components/CollectionDialog.test.js
+++ b/__tests__/src/components/CollectionDialog.test.js
@@ -5,7 +5,7 @@ import DialogActions from '@material-ui/core/DialogActions';
 import Button from '@material-ui/core/Button';
 import MenuItem from '@material-ui/core/MenuItem';
 import Skeleton from '@material-ui/lab/Skeleton';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { CollectionDialog } from '../../../src/components/CollectionDialog';
 import collection from '../../fixtures/version-2/collection.json';
 
diff --git a/__tests__/src/components/CompanionArea.test.js b/__tests__/src/components/CompanionArea.test.js
index 92414cc4d738acc76842516abd61c8509b9b8a4e..6e390ad13632ff646f0ee7c8c7996cc170aaaba3 100644
--- a/__tests__/src/components/CompanionArea.test.js
+++ b/__tests__/src/components/CompanionArea.test.js
@@ -1,6 +1,8 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import Slide from '@material-ui/core/Slide';
+import ArrowLeftIcon from '@material-ui/icons/ArrowLeftSharp';
+import ArrowRightIcon from '@material-ui/icons/ArrowRightSharp';
 import MiradorMenuButton from '../../../src/containers/MiradorMenuButton';
 import { CompanionArea } from '../../../src/components/CompanionArea';
 import CompanionWindowFactory from '../../../src/containers/CompanionWindowFactory';
@@ -69,7 +71,7 @@ describe('CompanionArea', () => {
     });
 
     expect(wrapper.find(MiradorMenuButton).length).toBe(1);
-    expect(wrapper.find(MiradorMenuButton).first().children('ArrowRightSharpIcon').length).toBe(1);
+    expect(wrapper.find(MiradorMenuButton).first().children(ArrowRightIcon).length).toBe(1);
     expect(wrapper.find(Slide).prop('direction')).toBe('right');
     expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(false);
     expect(wrapper.find('div.mirador-companion-windows').length).toBe(1);
@@ -91,7 +93,7 @@ describe('CompanionArea', () => {
     });
 
     expect(wrapper.find(MiradorMenuButton).length).toBe(1);
-    expect(wrapper.find(MiradorMenuButton).first().children('ArrowLeftSharpIcon').length).toBe(1);
+    expect(wrapper.find(MiradorMenuButton).first().children(ArrowLeftIcon).length).toBe(1);
     expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(true);
 
     expect(wrapper.find('div.mirador-companion-windows').length).toBe(1);
diff --git a/__tests__/src/components/FullScreenButton.test.js b/__tests__/src/components/FullScreenButton.test.js
index cdd961c03880bf52e3e64b7c960996d18dff7604..48701a823ad8859c11f73a1b14ee6bcfcc6c8d28 100644
--- a/__tests__/src/components/FullScreenButton.test.js
+++ b/__tests__/src/components/FullScreenButton.test.js
@@ -1,5 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
+import FullscreenIcon from '@material-ui/icons/FullscreenSharp';
+import FullscreenExitIcon from '@material-ui/icons/FullscreenExitSharp';
 import MiradorMenuButton from '../../../src/containers/MiradorMenuButton';
 import { FullScreenButton } from '../../../src/components/FullScreenButton';
 
@@ -36,7 +38,7 @@ describe('FullScreenButton', () => {
     });
 
     it('has the FullscreenIcon', () => {
-      expect(menuButton.children('FullscreenSharpIcon').length).toBe(1);
+      expect(menuButton.children(FullscreenIcon).length).toBe(1);
     });
 
     it('has the proper aria-label i18n key', () => {
@@ -58,7 +60,7 @@ describe('FullScreenButton', () => {
     });
 
     it('has the FullscreenExitIcon', () => {
-      expect(menuButton.children('FullscreenExitSharpIcon').length).toBe(1);
+      expect(menuButton.children(FullscreenExitIcon).length).toBe(1);
     });
 
     it('has the proper aria-label', () => {
diff --git a/__tests__/src/components/GalleryView.test.js b/__tests__/src/components/GalleryView.test.js
index ba03ef2c10805ee236f43faffb7ff0d4ad38abe6..4a15795c472795bbc44dc813aebd928954cdea30 100644
--- a/__tests__/src/components/GalleryView.test.js
+++ b/__tests__/src/components/GalleryView.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import Paper from '@material-ui/core/Paper';
 import manifestJson from '../../fixtures/version-2/019.json';
 import { GalleryView } from '../../../src/components/GalleryView';
diff --git a/__tests__/src/components/GalleryViewThumbnail.test.js b/__tests__/src/components/GalleryViewThumbnail.test.js
index 1dd5fb80bcc0f34d5219ded175bd5c985a983f0f..70ab6d78c3a58627245c642fc64b7bc16fce4589 100644
--- a/__tests__/src/components/GalleryViewThumbnail.test.js
+++ b/__tests__/src/components/GalleryViewThumbnail.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import Chip from '@material-ui/core/Chip';
 import IntersectionObserver from '@researchgate/react-intersection-observer';
 import manifestJson from '../../fixtures/version-2/019.json';
diff --git a/__tests__/src/components/IIIFThumbnail.test.js b/__tests__/src/components/IIIFThumbnail.test.js
index fa6eb72fa33ef5fb6e93b254b532ac2ede40cc57..5b60b112a9f825f34ae7f5d55713f0728d5bda00 100644
--- a/__tests__/src/components/IIIFThumbnail.test.js
+++ b/__tests__/src/components/IIIFThumbnail.test.js
@@ -72,11 +72,6 @@ describe('IIIFThumbnail', () => {
     expect(wrapper.find('img').props().style).toMatchObject({ height: 60, width: 50 });
   });
 
-  it('relaxes constraints when the image dimensions are unknown', () => {
-    wrapper = createWrapper({ thumbnail: { url } });
-    expect(wrapper.find('img').props().style).toMatchObject({ height: 'auto', width: 'auto' });
-  });
-
   it('constrains what it can when the image dimensions are unknown', () => {
     wrapper = createWrapper({ maxHeight: 90, thumbnail: { height: 120, url } });
     expect(wrapper.find('img').props().style).toMatchObject({ height: 90, width: 'auto' });
diff --git a/__tests__/src/components/LanguageSettings.test.js b/__tests__/src/components/LanguageSettings.test.js
index 88909f66fd5d400235e4b8120e44e2e10fc6bb08..ff1731013b2dbeb8deebaabcdfd88f22081b016b 100644
--- a/__tests__/src/components/LanguageSettings.test.js
+++ b/__tests__/src/components/LanguageSettings.test.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import ListItemText from '@material-ui/core/ListItemText';
 import MenuItem from '@material-ui/core/MenuItem';
+import CheckIcon from '@material-ui/icons/CheckSharp';
 import { LanguageSettings } from '../../../src/components/LanguageSettings';
 
 /**
@@ -63,7 +64,7 @@ describe('LanguageSettings', () => {
       wrapper
         .find(MenuItem)
         .first()
-        .find('CheckSharpIcon')
+        .find(CheckIcon)
         .length,
     ).toBe(1);
   });
diff --git a/__tests__/src/components/ManifestForm.test.js b/__tests__/src/components/ManifestForm.test.js
index cb4bb113ad7b3688622d055e76af0f6aa23bb549..4d170be7a8962c0ffe5f2692db9a75e9ba1d4d14 100644
--- a/__tests__/src/components/ManifestForm.test.js
+++ b/__tests__/src/components/ManifestForm.test.js
@@ -14,11 +14,14 @@ function createWrapper(props) {
 }
 
 describe('ManifestForm', () => {
-  it('renders', () => {
+  it('renders nothing if it is not open', () => {
     const wrapper = createWrapper({ addResourcesOpen: false });
+    expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(0);
+  });
+
+  it('renders the form fields', () => {
+    const wrapper = createWrapper({ addResourcesOpen: true });
     expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(1);
-    wrapper.setProps({ addResourcesOpen: true });
-    expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"] input').instance()).toEqual(document.activeElement);
     expect(wrapper.find('button[type="submit"]').length).toBe(1);
   });
 
diff --git a/__tests__/src/components/ManifestListItem.test.js b/__tests__/src/components/ManifestListItem.test.js
index 5a6620f1b6bc53b2f8414d1dce90c4b32cf0197a..bd75e02b3d4fe3b48169f61e54e84b0de41e1ea5 100644
--- a/__tests__/src/components/ManifestListItem.test.js
+++ b/__tests__/src/components/ManifestListItem.test.js
@@ -63,9 +63,9 @@ describe('ManifestListItem', () => {
     expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('ACME');
   });
 
-  it('displays a placeholder provider if no information is given', () => {
+  it('displays nothing  if no information is given', () => {
     const wrapper = createWrapper();
-    expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('addedFromUrl');
+    expect(wrapper.find('.mirador-manifest-list-item-provider').children().length).toEqual(0);
   });
 
   it('displays a collection label for collections', () => {
diff --git a/__tests__/src/components/NestedMenu.test.js b/__tests__/src/components/NestedMenu.test.js
index a2ce2bcd0f32e3d6cc65b453bdb014733f53af71..f6c49ba3cccc8b3865a1aefef80d8e5b953effa9 100644
--- a/__tests__/src/components/NestedMenu.test.js
+++ b/__tests__/src/components/NestedMenu.test.js
@@ -2,6 +2,8 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import ListItemText from '@material-ui/core/ListItemText';
+import ExpandLessIcon from '@material-ui/icons/ExpandLessSharp';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMoreSharp';
 import MenuItem from '@material-ui/core/MenuItem';
 import { NestedMenu } from '../../../src/components/NestedMenu';
 
@@ -61,11 +63,11 @@ describe('NestedMenu', () => {
     wrapper = createWrapper();
 
     expect(wrapper.state().nestedMenuIsOpen).toBe(false);
-    expect(wrapper.find('ExpandMoreSharpIcon').length).toBe(1);
-    expect(wrapper.find('ExpandLessSharpIcon').length).toBe(0);
+    expect(wrapper.find(ExpandMoreIcon).length).toBe(1);
+    expect(wrapper.find(ExpandLessIcon).length).toBe(0);
     wrapper.setState({ nestedMenuIsOpen: true });
-    expect(wrapper.find('ExpandMoreSharpIcon').length).toBe(0);
-    expect(wrapper.find('ExpandLessSharpIcon').length).toBe(1);
+    expect(wrapper.find(ExpandMoreIcon).length).toBe(0);
+    expect(wrapper.find(ExpandLessIcon).length).toBe(1);
   });
 
   it("renders the component's children based on the nestedMenuIsOpen state", () => {
diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js
index 46881f1da6097f1a84d03ef754554556b2c61a5d..7dfa0aae873b665d221d505927f71ee30034ec2b 100644
--- a/__tests__/src/components/OpenSeadragonViewer.test.js
+++ b/__tests__/src/components/OpenSeadragonViewer.test.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import OpenSeadragon from 'openseadragon';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer';
 import CanvasWorld from '../../../src/lib/CanvasWorld';
 import fixture from '../../fixtures/version-2/019.json';
diff --git a/__tests__/src/components/PrimaryWindow.test.js b/__tests__/src/components/PrimaryWindow.test.js
index d9961f9f14ef37777fb13314e9206f0a711996bc..47c41be28c49a31d30581ed24262dcfd2f4c1093 100644
--- a/__tests__/src/components/PrimaryWindow.test.js
+++ b/__tests__/src/components/PrimaryWindow.test.js
@@ -20,6 +20,13 @@ describe('PrimaryWindow', () => {
     const wrapper = createWrapper();
     expect(wrapper.find('.mirador-primary-window')).toHaveLength(1);
   });
+  it('should only render children when available', () => {
+    const wrapper = createWrapper({ children: <span>hi</span>, isFetching: false });
+    expect(wrapper.find('span')).toHaveLength(1);
+    const suspenseComponent = wrapper.find('Suspense');
+    const lazyComponent = suspenseComponent.dive().find('lazy');
+    expect(lazyComponent).toHaveLength(0);
+  });
   it('should render <WindowSideBar>', () => {
     const wrapper = createWrapper();
     expect(wrapper.find(WindowSideBar)).toHaveLength(1);
diff --git a/__tests__/src/components/SearchPanelControls.test.js b/__tests__/src/components/SearchPanelControls.test.js
index a4c14ee48511f3e46707134ebb6786fbb50687af..d09f641230cbb66e5cc7dcec22eaa22c2e3710db 100644
--- a/__tests__/src/components/SearchPanelControls.test.js
+++ b/__tests__/src/components/SearchPanelControls.test.js
@@ -4,6 +4,7 @@ import Autocomplete from '@material-ui/lab/Autocomplete';
 import CircularProgress from '@material-ui/core/CircularProgress';
 import Input from '@material-ui/core/Input';
 import TextField from '@material-ui/core/TextField';
+import SearchIcon from '@material-ui/icons/SearchSharp';
 import { SearchPanelControls } from '../../../src/components/SearchPanelControls';
 
 /**
@@ -56,7 +57,7 @@ describe('SearchPanelControls', () => {
       .dive()
       .dive();
     expect(divedInput.find(CircularProgress).length).toEqual(0);
-    expect(divedInput.find('SearchSharpIcon').length).toEqual(1);
+    expect(divedInput.find(SearchIcon).length).toEqual(1);
     expect(divedInput.find('Connect(WithPlugins(MiradorMenuButton))[type="submit"]').length).toEqual(1);
   });
 
diff --git a/__tests__/src/components/SidebarIndexList.test.js b/__tests__/src/components/SidebarIndexList.test.js
index 03cad91b6e943a2cf814c316b11e2667a94443e1..364a7646ad812bb8ecfcdc30d41b0604cfa526c1 100644
--- a/__tests__/src/components/SidebarIndexList.test.js
+++ b/__tests__/src/components/SidebarIndexList.test.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import MenuList from '@material-ui/core/MenuList';
 import MenuItem from '@material-ui/core/MenuItem';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { SidebarIndexList } from '../../../src/components/SidebarIndexList';
 import SidebarIndexItem from '../../../src/containers/SidebarIndexItem';
 import manifestJson from '../../fixtures/version-2/019.json';
diff --git a/__tests__/src/components/SidebarIndexTableOfContents.test.js b/__tests__/src/components/SidebarIndexTableOfContents.test.js
index 16722421136437be62f4e1a778d24d93fcbf1de3..15c3729ea513c20f2fd7c09197d20262e90e17aa 100644
--- a/__tests__/src/components/SidebarIndexTableOfContents.test.js
+++ b/__tests__/src/components/SidebarIndexTableOfContents.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import TreeItem from '@material-ui/lab/TreeItem';
 import TreeView from '@material-ui/lab/TreeView';
 import { SidebarIndexTableOfContents } from '../../../src/components/SidebarIndexTableOfContents';
diff --git a/__tests__/src/components/SidebarIndexThumbnail.test.js b/__tests__/src/components/SidebarIndexThumbnail.test.js
index a2c219a2f9df6def5bb5b21297121d070a623f7e..316bc8565d2d9a3ad396b6aaa431a929070ce85d 100644
--- a/__tests__/src/components/SidebarIndexThumbnail.test.js
+++ b/__tests__/src/components/SidebarIndexThumbnail.test.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import Typography from '@material-ui/core/Typography';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import fixture from '../../fixtures/version-2/019.json';
 import { SidebarIndexThumbnail } from '../../../src/components/SidebarIndexThumbnail';
 import IIIFThumbnail from '../../../src/containers/IIIFThumbnail';
diff --git a/__tests__/src/components/ThumbnailCanvasGrouping.test.js b/__tests__/src/components/ThumbnailCanvasGrouping.test.js
index ba27c5a71cbeaeab2f1a0c60f4c0c81271ef5ff8..23208396eab984161937f387444403778adff548 100644
--- a/__tests__/src/components/ThumbnailCanvasGrouping.test.js
+++ b/__tests__/src/components/ThumbnailCanvasGrouping.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { ThumbnailCanvasGrouping } from '../../../src/components/ThumbnailCanvasGrouping';
 import IIIFThumbnail from '../../../src/containers/IIIFThumbnail';
 import CanvasGroupings from '../../../src/lib/CanvasGroupings';
diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js
index 4bfbf173ff2859e039055e7ee0f7b78d9a7b9952..24940223925a16632dbd0a6dcbca8d0a7defd39a 100644
--- a/__tests__/src/components/ThumbnailNavigation.test.js
+++ b/__tests__/src/components/ThumbnailNavigation.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { ThumbnailNavigation } from '../../../src/components/ThumbnailNavigation';
 import ThumbnailCanvasGrouping from '../../../src/containers/ThumbnailCanvasGrouping';
 import CanvasGroupings from '../../../src/lib/CanvasGroupings';
diff --git a/__tests__/src/components/VideoViewer.test.js b/__tests__/src/components/VideoViewer.test.js
index 31945a749ea8a6206c0eed0bf9637e0241f2a24f..7b3521c08ac2bff116aff550356b69d5c3710bb7 100644
--- a/__tests__/src/components/VideoViewer.test.js
+++ b/__tests__/src/components/VideoViewer.test.js
@@ -7,6 +7,7 @@ function createWrapper(props, suspenseFallback) {
   return shallow(
     <VideoViewer
       classes={{}}
+      videoOptions={{ crossOrigin: 'anonymous' }}
       {...props}
     />,
   );
@@ -22,21 +23,29 @@ describe('VideoViewer', () => {
           { getFormat: () => 'video/mp4', id: 2 },
         ],
       }, true);
-      expect(wrapper.contains(<source src="1" type="video/mp4" />));
-      expect(wrapper.contains(<source src="2" type="video/mp4" />));
+      expect(wrapper.contains(<source src={1} type="video/mp4" />)).toBe(true);
+      expect(wrapper.contains(<source src={2} type="video/mp4" />)).toBe(true);
+    });
+    it('passes through configurable options', () => {
+      wrapper = createWrapper({
+        videoResources: [
+          { getFormat: () => 'video/mp4', id: 1 },
+        ],
+      }, true);
+      expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption
     });
     it('captions', () => {
       wrapper = createWrapper({
         captions: [
-          { getLabel: () => 'English', getProperty: () => 'en', id: 1 },
-          { getLabel: () => 'French', getProperty: () => 'fr', id: 2 },
+          { getDefaultLabel: () => 'English', getProperty: () => 'en', id: 1 },
+          { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 },
         ],
         videoResources: [
           { getFormat: () => 'video/mp4', id: 1 },
         ],
       }, true);
-      expect(wrapper.contains(<track src="1" label="English" srcLang="en" />));
-      expect(wrapper.contains(<track src="2" label="French" srcLang="fr" />));
+      expect(wrapper.contains(<track src={1} label="English" srcLang="en" />)).toBe(true);
+      expect(wrapper.contains(<track src={2} label="French" srcLang="fr" />)).toBe(true);
     });
   });
 });
diff --git a/__tests__/src/components/ViewerNavigation.test.js b/__tests__/src/components/ViewerNavigation.test.js
index 3ffe5b2657b6f6c4504fa8f0c55d7863c8b9b3f5..36ffe068bdeae5cc577e68b58754d195717de906 100644
--- a/__tests__/src/components/ViewerNavigation.test.js
+++ b/__tests__/src/components/ViewerNavigation.test.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
+import NavigationIcon from '@material-ui/icons/PlayCircleOutlineSharp';
 import MiradorMenuButton from '../../../src/containers/MiradorMenuButton';
 import { ViewerNavigation } from '../../../src/components/ViewerNavigation';
 
@@ -89,8 +90,8 @@ describe('ViewerNavigation', () => {
     });
 
     it('changes the arrow styles', () => {
-      const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props();
-      const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props();
+      const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props();
+      const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props();
       expect(previous.style).toEqual({});
       expect(next.style).toEqual({ transform: 'rotate(180deg)' });
     });
@@ -112,8 +113,8 @@ describe('ViewerNavigation', () => {
     });
 
     it('changes the arrow styles', () => {
-      const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props();
-      const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props();
+      const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props();
+      const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props();
       expect(previous.style).toEqual({ transform: 'rotate(270deg)' });
       expect(next.style).toEqual({ transform: 'rotate(90deg)' });
     });
@@ -131,8 +132,8 @@ describe('ViewerNavigation', () => {
     });
 
     it('changes the arrow styles', () => {
-      const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props();
-      const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props();
+      const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props();
+      const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props();
       expect(previous.style).toEqual({ transform: 'rotate(90deg)' });
       expect(next.style).toEqual({ transform: 'rotate(270deg)' });
     });
diff --git a/__tests__/src/components/WindowSideBarCanvasPanel.test.js b/__tests__/src/components/WindowSideBarCanvasPanel.test.js
index 3d2c96ea5022ea470b3c80b61283d83dd5bd57fe..12ed93e36acaf9fa50ec20f8bb27fc5eccdfe454 100644
--- a/__tests__/src/components/WindowSideBarCanvasPanel.test.js
+++ b/__tests__/src/components/WindowSideBarCanvasPanel.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import compact from 'lodash/compact';
 import { WindowSideBarCanvasPanel } from '../../../src/components/WindowSideBarCanvasPanel';
 import SidebarIndexList from '../../../src/containers/SidebarIndexList';
diff --git a/__tests__/src/components/WorkspaceControlPanelButtons.test.js b/__tests__/src/components/WorkspaceControlPanelButtons.test.js
index 37c7eaf107fbada564310ce626a03ac3e9cf214f..c94ecdd0d08568ec3774ee447a6f221a7ed70eaa 100644
--- a/__tests__/src/components/WorkspaceControlPanelButtons.test.js
+++ b/__tests__/src/components/WorkspaceControlPanelButtons.test.js
@@ -2,8 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import WorkspaceMenuButton from '../../../src/containers/WorkspaceMenuButton';
 import FullScreenButton from '../../../src/containers/FullScreenButton';
-import { WorkspaceControlPanelButtons }
-  from '../../../src/components/WorkspaceControlPanelButtons';
+import { WorkspaceControlPanelButtons } from '../../../src/components/WorkspaceControlPanelButtons';
 import { PluginHook } from '../../../src/components/PluginHook';
 
 describe('WorkspaceControlPanelButtons', () => {
diff --git a/__tests__/src/extend/withPlugins.test.js b/__tests__/src/extend/withPlugins.test.js
index ac80f7de3f6d90b9908eb5e8dca786f6bb2aeebe..7fdda8ce5649b84a9f263908667350badd0da8dc 100644
--- a/__tests__/src/extend/withPlugins.test.js
+++ b/__tests__/src/extend/withPlugins.test.js
@@ -126,6 +126,7 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => {
   });
   it('renders the first wrap plugin, renders add plugins if plugin/props are passed through', () => {
     /** */ const WrapPluginComponentA = plugin => (
+      // eslint-disable-next-line react/destructuring-assignment
       <plugin.TargetComponent {...plugin.targetProps} {...plugin} />
     );
     /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>;
diff --git a/__tests__/src/lib/CanvasAnnotationDisplay.test.js b/__tests__/src/lib/CanvasAnnotationDisplay.test.js
index ec4e3d80eda461d2eb4ca6619e215989f581805e..721ef34a6f9440ba37d120857eae9ad10098e17a 100644
--- a/__tests__/src/lib/CanvasAnnotationDisplay.test.js
+++ b/__tests__/src/lib/CanvasAnnotationDisplay.test.js
@@ -34,7 +34,7 @@ describe('CanvasAnnotationDisplay', () => {
       expect(subject.svgContext).toHaveBeenCalled();
       expect(subject.fragmentContext).not.toHaveBeenCalled();
     });
-    it('selects fragmentSelector if no svg present', () => {
+    it('selects fragmentSelector if present and if no svg is present', () => {
       const context = {
         stroke: jest.fn(),
       };
@@ -47,6 +47,19 @@ describe('CanvasAnnotationDisplay', () => {
       expect(subject.svgContext).not.toHaveBeenCalled();
       expect(subject.fragmentContext).toHaveBeenCalled();
     });
+    it('ignores annotations without selectors', () => {
+      const context = {
+        stroke: jest.fn(),
+      };
+      const subject = createSubject({
+        resource: new AnnotationResource({ on: 'www.example.com' }),
+      });
+      subject.svgContext = jest.fn();
+      subject.fragmentContext = jest.fn();
+      subject.toContext(context);
+      expect(subject.svgContext).not.toHaveBeenCalled();
+      expect(subject.fragmentContext).not.toHaveBeenCalled();
+    });
   });
   describe('svgString', () => {
     it('selects the svg selector string value', () => {
diff --git a/__tests__/src/lib/CanvasWorld.test.js b/__tests__/src/lib/CanvasWorld.test.js
index ac755e202f11c636943b5f8f8b837cbcb1fc1950..cabd13a630eb787c2711431f882376b0b6428b55 100644
--- a/__tests__/src/lib/CanvasWorld.test.js
+++ b/__tests__/src/lib/CanvasWorld.test.js
@@ -1,4 +1,4 @@
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import fixture from '../../fixtures/version-2/019.json';
 import fragmentFixture from '../../fixtures/version-2/hamilton.json';
 import CanvasWorld from '../../../src/lib/CanvasWorld';
diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js
index e40237b59748f58b00f5575ef9fc99dce49c7f03..0a41c260c327c832beb69823429cffc2a6e487eb 100644
--- a/__tests__/src/lib/MiradorCanvas.test.js
+++ b/__tests__/src/lib/MiradorCanvas.test.js
@@ -1,4 +1,4 @@
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import MiradorCanvas from '../../../src/lib/MiradorCanvas';
 import fixture from '../../fixtures/version-2/019.json';
 import serviceFixture from '../../fixtures/version-2/canvasService.json';
diff --git a/__tests__/src/lib/MiradorManifest.test.js b/__tests__/src/lib/MiradorManifest.test.js
index d4ffe0bcf1cff5e5e97b385859a837ff71c723dc..7832fa97e1df4d85f9df6e1f5e1de3ad3b1cb6b6 100644
--- a/__tests__/src/lib/MiradorManifest.test.js
+++ b/__tests__/src/lib/MiradorManifest.test.js
@@ -1,4 +1,4 @@
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import MiradorManifest from '../../../src/lib/MiradorManifest';
 
 /** */
diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js
index 1b260abf57105af118f35a73c0fee33e37e707aa..f3f2702b66bfb4d40bcdf3e0851332257b56ae49 100644
--- a/__tests__/src/lib/MiradorViewer.test.js
+++ b/__tests__/src/lib/MiradorViewer.test.js
@@ -1,4 +1,5 @@
 import ReactDOM from 'react-dom';
+import { shallow } from 'enzyme';
 import MiradorViewer from '../../../src/lib/MiradorViewer';
 
 jest.unmock('react-i18next');
@@ -10,7 +11,7 @@ describe('MiradorViewer', () => {
   beforeAll(() => {
     ReactDOM.render = jest.fn();
     ReactDOM.unmountComponentAtNode = jest.fn();
-    instance = new MiradorViewer({});
+    instance = new MiradorViewer({ id: 'mirador' });
   });
   describe('constructor', () => {
     it('returns viewer store', () => {
@@ -104,6 +105,15 @@ describe('MiradorViewer', () => {
       }));
     });
   });
+
+  describe('render', () => {
+    it('passes props through to the App component', () => {
+      const rendered = shallow(instance.render({ some: 'prop' }));
+      expect(rendered.find('App').length).toBe(1);
+      expect(rendered.find('App').prop('some')).toBe('prop');
+    });
+  });
+
   describe('unmount', () => {
     it('unmounts via ReactDOM', () => {
       instance.unmount();
diff --git a/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js
index 23a785b56d3cbaf971b7f3748e6678045a16de64..22a661e7234a93a615d52dd272f0e0de8b1f68dd 100644
--- a/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js
+++ b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js
@@ -17,7 +17,7 @@ describe('OpenSeadragonCanvasOverlay', () => {
         clientWidth: 200,
       },
       viewport: {
-        getBoundsNoRotate: jest.fn(() => ({
+        getBoundsNoRotateWithMargins: jest.fn(() => ({
           height: 300,
           width: 200,
           x: 40,
@@ -92,7 +92,7 @@ describe('OpenSeadragonCanvasOverlay', () => {
           clientWidth: 200,
         },
         viewport: {
-          getBoundsNoRotate: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))),
+          getBoundsNoRotateWithMargins: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))),
         },
         world: {
           getItemAt: jest.fn(),
diff --git a/__tests__/src/lib/ThumbnailFactory.test.js b/__tests__/src/lib/ThumbnailFactory.test.js
index 42344b82f0d1e649dfa6dd52405aab582b149ccb..6f0cc3cfe934558f837bac5437d4fb36f66c3e7e 100644
--- a/__tests__/src/lib/ThumbnailFactory.test.js
+++ b/__tests__/src/lib/ThumbnailFactory.test.js
@@ -1,12 +1,17 @@
-import { ManifestResource, Resource, Utils } from 'manifesto.js/dist-esmodule';
-import getThumbnail from '../../../src/lib/ThumbnailFactory';
+import {
+  ManifestResource, Resource, Service, Utils,
+} from 'manifesto.js';
+import getThumbnail, { ThumbnailFactory } from '../../../src/lib/ThumbnailFactory';
 import fixture from '../../fixtures/version-2/019.json';
 
 const manifest = Utils.parseManifest(fixture);
 const canvas = manifest.getSequences()[0].getCanvases()[0];
 
 /** */
-function createSubject(jsonld, iiifOpts) {
+function createSubject(jsonld, resourceType, iiifOpts) {
+  if (resourceType === 'Image') {
+    return createImageSubject(jsonld, iiifOpts);
+  }
   return getThumbnail(new ManifestResource(jsonld, {}), iiifOpts);
 }
 
@@ -36,37 +41,47 @@ describe('getThumbnail', () => {
   const iiifLevel0Service = iiifService(url, {}, { profile: 'level0' });
   const iiifLevel1Service = iiifService(url, { height: 2000, width: 1000 }, { profile: 'level1' });
   const iiifLevel2Service = iiifService(url, { height: 2000, width: 1000 }, { profile: 'level2' });
+  const sizes = [
+    { height: 25, width: 25 },
+    { height: 100, width: 100 },
+    { height: 125, width: 125 },
+    { height: 1000, width: 1000 },
+  ];
 
-  describe('with a thumbnail', () => {
-    it('return the thumbnail and metadata', () => {
-      expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: { '@id': url, height: 70, width: 50 } })).toMatchObject({ height: 70, url, width: 50 });
-    });
+  describe('with a IIIF resource', () => {
+    for (const type of ['Collection', 'Manifest', 'Canvas', 'Image']) {
+      describe('with a thumbnail', () => {
+        it('return the thumbnail and metadata', () => {
+          expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: { '@id': url, height: 70, width: 50 } }, type)).toMatchObject({ height: 70, url, width: 50 });
+        });
 
-    it('return the IIIF service of the thumbnail', () => {
-      expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel1Service })).toMatchObject({ url: `${url}/full/,120/0/default.jpg` });
-    });
+        it('return the IIIF service of the thumbnail', () => {
+          expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel1Service }, type)).toMatchObject({ url: `${url}/full/,120/0/default.jpg` });
+        });
 
-    describe('with image size constraints', () => {
-      it('does nothing with a static resource', () => {
-        expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: { '@id': url } }, { maxWidth: 50 })).toMatchObject({ url });
-      });
+        describe('with image size constraints', () => {
+          it('does nothing with a static resource', () => {
+            expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: { '@id': url } }, type, { maxWidth: 50 })).toMatchObject({ url });
+          });
 
-      it('does nothing with a IIIF level 0 service', () => {
-        expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel0Service }, { maxWidth: 50 })).toMatchObject({ url: 'arbitrary-url' });
-      });
+          it('does nothing with a IIIF level 0 service', () => {
+            expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel0Service }, type, { maxWidth: 50 })).toMatchObject({ url: 'arbitrary-url' });
+          });
 
-      it('calculates constraints for a IIIF level 1 service', () => {
-        expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel1Service }, { maxWidth: 150 })).toMatchObject({ height: 300, url: `${url}/full/150,/0/default.jpg`, width: 150 });
-      });
+          it('calculates constraints for a IIIF level 1 service', () => {
+            expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel1Service }, type, { maxWidth: 150 })).toMatchObject({ height: 300, url: `${url}/full/150,/0/default.jpg`, width: 150 });
+          });
 
-      it('calculates constraints for a IIIF level 2 service', () => {
-        expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel2Service }, { maxHeight: 200, maxWidth: 150 })).toMatchObject({ height: 200, url: `${url}/full/!150,200/0/default.jpg`, width: 100 });
-      });
+          it('calculates constraints for a IIIF level 2 service', () => {
+            expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel2Service }, type, { maxHeight: 200, maxWidth: 150 })).toMatchObject({ height: 200, url: `${url}/full/!150,200/0/default.jpg`, width: 100 });
+          });
 
-      it('applies a minumum size to image constraints to encourage asset reuse', () => {
-        expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel2Service }, { maxHeight: 100, maxWidth: 100 })).toMatchObject({ height: 120, url: `${url}/full/!120,120/0/default.jpg`, width: 60 });
+          it('applies a minumum size to image constraints to encourage asset reuse', () => {
+            expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel2Service }, type, { maxHeight: 100, maxWidth: 100 })).toMatchObject({ height: 120, url: `${url}/full/!120,120/0/default.jpg`, width: 60 });
+          });
+        });
       });
-    });
+    }
   });
 
   describe('with an image resource', () => {
@@ -86,12 +101,6 @@ describe('getThumbnail', () => {
       });
 
       it('uses embedded sizes to find an appropriate size', () => {
-        const sizes = [
-          { height: 25, width: 25 },
-          { height: 100, width: 100 },
-          { height: 125, width: 125 },
-          { height: 1000, width: 1000 },
-        ];
         const obj = {
           ...(iiifService('some-url', {}, { profile: 'level0', sizes })),
           id: 'xyz',
@@ -125,15 +134,55 @@ describe('getThumbnail', () => {
 
   describe('with a canvas', () => {
     it('uses the thumbnail', () => {
-      expect(createSubject({ ...canvas.__jsonld, thumbnail: { ...iiifLevel1Service } })).toMatchObject({ url: `${url}/full/,120/0/default.jpg` });
+      expect(createSubject({ ...canvas.__jsonld, thumbnail: { ...iiifLevel1Service } }, 'Canvas')).toMatchObject({ url: `${url}/full/,120/0/default.jpg` });
     });
 
     it('uses the first image resource', () => {
       expect(getThumbnail(canvas)).toMatchObject({ url: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/,120/0/default.jpg' });
     });
+
+    it('uses the width and height of a thumbnail without a IIIF Image API service', () => {
+      const myCanvas = {
+        ...canvas.__jsonld,
+        thumbnail: {
+          height: 240,
+          id: 'arbitrary-url',
+          width: 180,
+        },
+      };
+      expect(createSubject(myCanvas, 'Canvas')).toMatchObject({ height: 240, url: 'arbitrary-url', width: 180 });
+    });
+
+    it('uses embedded sizes of a IIIF Image API service to find an appropriate size', () => {
+      const myCanvas = {
+        ...canvas.__jsonld,
+        thumbnail: {
+          height: 100,
+          id: 'arbitrary-url',
+          service: [{
+            id: url,
+            profile: 'level2',
+            sizes,
+            type: 'ImageService3',
+          }],
+          width: 100,
+        },
+      };
+      expect(createSubject(myCanvas, 'Canvas', { maxHeight: 120, maxWidth: 120 }))
+        .toMatchObject({ height: 125, url: `${url}/full/125,125/0/default.jpg`, width: 125 });
+    });
   });
 
   describe('with a manifest', () => {
+    it('does nothing with a plain URL', () => {
+      const manifestWithThumbnail = Utils.parseManifest({
+        ...manifest.__jsonld,
+        thumbnail: url,
+      });
+
+      expect(getThumbnail(manifestWithThumbnail, { maxWidth: 50 })).toMatchObject({ url });
+    });
+
     it('uses the thumbnail', () => {
       const manifestWithThumbnail = Utils.parseManifest({
         ...manifest.__jsonld,
@@ -198,3 +247,102 @@ describe('getThumbnail', () => {
     });
   });
 });
+
+describe('picking the best format', () => {
+  const url = 'http://example.com';
+
+  it('defaults to jpg', () => {
+    const myCanvas = {
+      ...canvas.__jsonld,
+      thumbnail: {
+        height: 100,
+        id: 'arbitrary-url',
+        service: [{
+          id: url,
+          profile: 'level2',
+          type: 'ImageService3',
+        }],
+        width: 100,
+      },
+    };
+    expect(createSubject(myCanvas, 'Canvas'))
+      .toMatchObject({ url: `${url}/full/,120/0/default.jpg` });
+  });
+
+  it('uses the preferred format of the service', () => {
+    const myCanvas = {
+      ...canvas.__jsonld,
+      thumbnail: {
+        height: 100,
+        id: 'arbitrary-url',
+        service: [{
+          id: url,
+          preferredFormats: ['webp'],
+          profile: 'level2',
+          type: 'ImageService3',
+        }],
+        width: 100,
+      },
+    };
+    expect(createSubject(myCanvas, 'Canvas'))
+      .toMatchObject({ url: `${url}/full/,120/0/default.webp` });
+  });
+
+  it('can be filtered by application preferred formats', () => {
+    const myCanvas = {
+      ...canvas.__jsonld,
+      thumbnail: {
+        height: 100,
+        id: 'arbitrary-url',
+        service: [{
+          id: url,
+          preferredFormats: ['webp', 'png'],
+          profile: 'level2',
+          type: 'ImageService3',
+        }],
+        width: 100,
+      },
+    };
+    expect(createSubject(myCanvas, 'Canvas', { preferredFormats: ['png', 'jpg'] }))
+      .toMatchObject({ url: `${url}/full/,120/0/default.png` });
+  });
+});
+
+describe('selectBestImageSize', () => {
+  const targetWidth = 120;
+  const targetHeight = 120;
+
+  it('selects the smallest size larger than the target, if one is available', () => {
+    const sizes = [
+      { height: 75, width: 75 },
+      { height: 150, width: 150 },
+      { height: 300, width: 300 },
+    ];
+    const service = new Service({
+      id: 'arbitrary-url',
+      profile: 'level0',
+      sizes,
+      type: 'ImageService3',
+    });
+
+    expect(ThumbnailFactory.selectBestImageSize(service, targetWidth * targetHeight))
+      .toEqual(sizes[1]);
+  });
+
+  it('selects the largest size smaller than the target, if none larger are available', () => {
+    const sizes = [
+      { height: 25, width: 25 },
+      { height: 50, width: 50 },
+      { height: 75, width: 75 },
+    ];
+    const service = new Service({
+      id: 'arbitrary-url',
+      profile: 'level0',
+      sizes,
+      type: 'ImageService3',
+    });
+
+    expect(ThumbnailFactory.selectBestImageSize(service, targetWidth * targetHeight))
+      .toEqual(sizes[2]);
+  });
+});
diff --git a/__tests__/src/sagas/auth.test.js b/__tests__/src/sagas/auth.test.js
index 229e9956e3a166de4ab843e322029fb1a05b4e12..8d31a5b445c18e70fd666b2ea4ceeee6d8002df3 100644
--- a/__tests__/src/sagas/auth.test.js
+++ b/__tests__/src/sagas/auth.test.js
@@ -1,6 +1,6 @@
 import { call, select } from 'redux-saga/effects';
 import { expectSaga } from 'redux-saga-test-plan';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import serviceFixture from '../../fixtures/version-2/canvasService.json';
 import settings from '../../../src/config/settings';
 import ActionTypes from '../../../src/state/actions/action-types';
diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js
index 7a69c828b640ae383aba6a9759e2cf0b5b94720f..796c0adc0072fecef6b2290c6bdd649232d08d32 100644
--- a/__tests__/src/sagas/windows.test.js
+++ b/__tests__/src/sagas/windows.test.js
@@ -1,6 +1,6 @@
 import { call, select } from 'redux-saga/effects';
 import { expectSaga } from 'redux-saga-test-plan';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 
 import ActionTypes from '../../../src/state/actions/action-types';
 import { setCanvas } from '../../../src/state/actions';
@@ -14,6 +14,7 @@ import {
   getSortedSearchAnnotationsForCompanionWindow,
   getVisibleCanvasIds, getCanvasForAnnotation,
   getCanvases, selectInfoResponses,
+  getWindowConfig,
 } from '../../../src/state/selectors';
 import { fetchManifests } from '../../../src/state/sagas/iiif';
 import {
@@ -343,6 +344,7 @@ describe('window-level sagas', () => {
 
       return expectSaga(setCanvasOfFirstSearchResult, action)
         .provide([
+          [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: true }],
           [select(getSelectedContentSearchAnnotationIds, { companionWindowId, windowId }), []],
           [select(getSortedSearchAnnotationsForCompanionWindow, { companionWindowId, windowId }), [{ id: 'a' }, { id: 'b' }]],
         ])
@@ -365,10 +367,27 @@ describe('window-level sagas', () => {
 
       return expectSaga(setCanvasOfFirstSearchResult, action)
         .provide([
+          [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: true }],
           [select(getSelectedContentSearchAnnotationIds, { companionWindowId, windowId }), ['y']],
         ])
         .run().then(({ allEffects }) => allEffects.length === 0);
     });
+
+    it('does nothing if canvas switching for searches is disabled', () => {
+      const companionWindowId = 'x';
+      const windowId = 'y';
+      const action = {
+        companionWindowId,
+        type: ActionTypes.RECEIVE_SEARCH,
+        windowId,
+      };
+
+      return expectSaga(setCanvasOfFirstSearchResult, action)
+        .provide([
+          [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: false }],
+        ])
+        .run().then(({ allEffects }) => allEffects.length === 0);
+    });
   });
 
   describe('setCanvasforSelectedAnnotation', () => {
diff --git a/__tests__/src/selectors/manifests.test.js b/__tests__/src/selectors/manifests.test.js
index 0251ccb49a847c5ab968153a7dedffa58de37681..3c174506f35b8e0969d177dae3c8652e885a4231 100644
--- a/__tests__/src/selectors/manifests.test.js
+++ b/__tests__/src/selectors/manifests.test.js
@@ -1,4 +1,4 @@
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import manifestFixture001 from '../../fixtures/version-2/001.json';
 import manifestFixture002 from '../../fixtures/version-2/002.json';
 import manifestFixture019 from '../../fixtures/version-2/019.json';
diff --git a/config/paths.js b/config/paths.js
deleted file mode 100644
index 2060badce016cbe023d2af06e9a9015114a77858..0000000000000000000000000000000000000000
--- a/config/paths.js
+++ /dev/null
@@ -1,98 +0,0 @@
-const path = require('path');
-const fs = require('fs');
-const url = require('url');
-
-// Make sure any symlinks in the project folder are resolved:
-// https://github.com/facebook/create-react-app/issues/637
-const appDirectory = fs.realpathSync(process.cwd());
-
-/**
- *
- * @param relativePath
- * @returns {string}
- */
-const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
-
-const envPublicUrl = process.env.PUBLIC_URL;
-
-/**
- *
- * @param inputPath
- * @param needsSlash
- * @returns {*}
- */
-function ensureSlash(inputPath, needsSlash) {
-  const hasSlash = inputPath.endsWith('/');
-  if (hasSlash && !needsSlash) {
-    return inputPath.substr(0, inputPath.length - 1);
-  } else if (!hasSlash && needsSlash) {
-    return `${inputPath}/`;
-  } else {
-    return inputPath;
-  }
-}
-
-/**
- *
- * @param appPackageJson
- * @returns {string | *}
- */
-const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage;
-
-/**
- *
- * @param appPackageJson
- * @returns {*}
- */
-function getServedPath(appPackageJson) {
-  const publicUrl = getPublicUrl(appPackageJson);
-  const servedUrl =
-    envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
-  return ensureSlash(servedUrl, true);
-}
-
-const moduleFileExtensions = [
-  'web.mjs',
-  'mjs',
-  'web.js',
-  'js',
-  'web.ts',
-  'ts',
-  'web.tsx',
-  'tsx',
-  'json',
-  'web.jsx',
-  'jsx',
-];
-
-/**
- *
- * @param resolveFn
- * @param filePath
- * @returns {*}
- */
-const resolveModule = (resolveFn, filePath) => {
-  const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`)));
-
-  if (extension) {
-    return resolveFn(`${filePath}.${extension}`);
-  }
-
-  return resolveFn(`${filePath}.js`);
-};
-
-module.exports = {
-  dotenv: resolveApp('.env'),
-  appPath: resolveApp('.'),
-  appBuild: resolveApp('build'),
-  appDist: resolveApp('dist'),
-  appIndexJs: resolveModule(resolveApp, 'src/index'),
-  appPackageJson: resolveApp('package.json'),
-  appSrc: resolveApp('src'),
-  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
-  appNodeModules: resolveApp('node_modules'),
-  publicUrl: getPublicUrl(resolveApp('package.json')),
-  servedPath: getServedPath(resolveApp('package.json')),
-};
-
-module.exports.moduleFileExtensions = moduleFileExtensions;
diff --git a/jest.json b/jest.json
index fd8cf635a7e1b28ed051bcdc24850581f52e12cd..d635140982349d0f2b6e906c1b113cdc4ab04b88 100644
--- a/jest.json
+++ b/jest.json
@@ -17,8 +17,5 @@
     "<rootDir>/**/__tests__/**/*.{js,jsx}",
     "<rootDir>/src/**/?(*.)(spec|test|unit).{js,jsx}"
   ],
-  "transformIgnorePatterns": [
-    "<rootDir>/node_modules/(?!manifesto.js)"
-  ],
   "preset": "jest-puppeteer"
 }
diff --git a/package.json b/package.json
index 0e6fbf83457231eb331e533f029a2c7d0d14daae..ebb453fd4179d03b0f6aa9d9b49d706417d6c293 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "mirador",
-  "version": "3.0.0",
+  "version": "3.3.0",
   "description": "An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.",
-  "main": "dist/mirador.min.js",
+  "main": "dist/cjs/src/index.js",
   "module": "dist/es/src/index.js",
   "files": [
     "dist"
@@ -10,10 +10,12 @@
   "sideEffects": false,
   "scripts": {
     "clean": "rm -rf ./dist",
-    "lint": "node_modules/.bin/eslint ./ && npm run lint:translations",
+    "lint": "node_modules/.bin/eslint ./ && npm run lint:translations && npm run lint:containers",
+    "lint:containers": "node ./scripts/container-lint.js",
     "lint:translations": "node ./scripts/i18n-lint.js",
     "server": "node_modules/.bin/http-server --cors",
     "test": "npm run build && npm run lint && npm run size && jest -c jest.json",
+    "test:debug": "node --inspect node_modules/.bin/jest -c jest.json --runInBand",
     "test:watch": "jest -c jest.json --watch",
     "build": "NODE_ENV=production webpack --mode=production",
     "build:dev": "webpack --mode=development",
@@ -22,7 +24,7 @@
     "build:watch": "webpack --watch --mode=development",
     "prepublishOnly": "npm run clean && npm run build:es && npm run build:cjs && npm run build",
     "size": "bundlewatch --config bundlewatch.config.json",
-    "start": "NODE_ENV=development webpack-dev-server --open"
+    "start": "NODE_ENV=development webpack serve --open"
   },
   "license": "Apache-2.0",
   "contributors": [
@@ -32,7 +34,7 @@
   "repository": "https://github.com/ProjectMirador/mirador",
   "dependencies": {
     "@material-ui/core": "^4.11.0",
-    "@material-ui/icons": "~4.9.1",
+    "@material-ui/icons": "^4.9.1",
     "@material-ui/lab": "^4.0.0-alpha.53",
     "@researchgate/react-intersection-observer": "^1.0.0",
     "classnames": "^2.2.6",
@@ -68,7 +70,7 @@
     "react-sizeme": "^2.6.7",
     "react-virtualized-auto-sizer": "^1.0.2",
     "react-window": "^1.8.5",
-    "redux": "4.0.5",
+    "redux": "^4.0.5",
     "redux-devtools-extension": "^2.13.2",
     "redux-saga": "^1.1.3",
     "redux-thunk": "^2.3.0",
@@ -84,47 +86,48 @@
     "@babel/plugin-transform-runtime": "^7.10.3",
     "@babel/preset-env": "^7.10.3",
     "@babel/preset-react": "^7.10.1",
-    "@pmmmwh/react-refresh-webpack-plugin": "^0.3.3",
-    "babel-eslint": "10.1.0",
+    "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
+    "@typescript-eslint/eslint-plugin": "^4.21.0",
+    "@typescript-eslint/parser": "^4.21.0",
+    "babel-eslint": "^10.1.0",
     "babel-jest": "^26.0.1",
     "babel-loader": "^8.0.6",
     "babel-plugin-lodash": "^3.3.4",
-    "babel-plugin-macros": "^2.8.0",
+    "babel-plugin-macros": "^3.0.1",
     "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
-    "bundlewatch": "^0.2.5",
+    "bundlewatch": "^0.3.2",
     "chalk": "^4.1.0",
     "codecov": "^3.7.0",
     "core-js": "^3.4.8",
     "enzyme": "^3.10.0",
-    "enzyme-adapter-react-16": "^1.14.0",
-    "eslint": "^6.0.0",
+    "enzyme-adapter-react-16": "^1.15.0",
+    "eslint": "^7.23.0",
     "eslint-config-airbnb": "^18.2.0",
-    "eslint-config-react-app": "^3.0.5",
+    "eslint-config-react-app": "^6.0.0",
     "eslint-loader": "^4.0.2",
-    "eslint-plugin-flowtype": "^4.7.0",
-    "eslint-plugin-import": "^2.18.0",
-    "eslint-plugin-jest": "^23.16.0",
-    "eslint-plugin-jsx-a11y": "^6.2.3",
-    "eslint-plugin-react": "^7.14.2",
+    "eslint-plugin-flowtype": "^5.6.0",
+    "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-jest": "^24.0.0",
+    "eslint-plugin-jsx-a11y": "^6.4.1",
+    "eslint-plugin-react": "^7.23.2",
+    "eslint-plugin-react-hooks": "^4.2.0",
     "glob": "^7.1.4",
     "http-server": "^0.12.3",
     "jest": "^26.0.1",
     "jest-fetch-mock": "^3.0.0",
-    "jest-puppeteer": "^4.1.1",
-    "jsdom": "15.1.1",
-    "puppeteer": "^4.0.0",
+    "jest-puppeteer": "^5.0.2",
+    "jsdom": "^16.5.3",
+    "puppeteer": "^9.0.0",
     "react": "^16.8.6",
-    "react-dev-utils": "^10.2.1",
     "react-dom": "^16.8.6",
     "react-refresh": "^0.8.3",
     "redux-mock-store": "^1.5.1",
     "redux-saga-test-plan": "^4.0.0-rc.3",
-    "supertest": "^4.0.2",
-    "terser-webpack-plugin": "^3.0.6",
+    "terser-webpack-plugin": "^4.0.0",
     "unfetch": "^4.1.0",
     "url-polyfill": "^1.1.7",
     "webpack": "^4.43.0",
-    "webpack-cli": "^3.3.5",
+    "webpack-cli": "^4.6.0",
     "webpack-dev-server": "^3.11.0"
   },
   "peerDependencies": {
diff --git a/scripts/container-lint.js b/scripts/container-lint.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ef296ba57479c6c643c602e23a3042d50fce3ae
--- /dev/null
+++ b/scripts/container-lint.js
@@ -0,0 +1,18 @@
+const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies
+const fs = require('fs');
+const chalk = require('chalk'); // eslint-disable-line import/no-extraneous-dependencies
+
+const { error } = console;
+const globOpts = { cwd: 'src/containers' };
+const files = glob.sync('**/*.js', globOpts);
+
+files.forEach((fileName) => {
+  const fileContent = fs.readFileSync(`src/containers/${fileName}`).toString();
+  const withPlugins = fileContent.indexOf('withPlugins(');
+  if (withPlugins > 0) {
+    const correctCall = fileContent.indexOf(`withPlugins('${fileName.replace('.js', '')}')`);
+    if (withPlugins !== correctCall) {
+      error(chalk.red(`Check withPlugins for ${fileName} for an incorrect target`));
+    }
+  }
+});
diff --git a/src/components/AudioViewer.js b/src/components/AudioViewer.js
index 85d9548c8850ed03414e8a03cf72d1fcdcc2e52d..fd63c4053fb73af660192e406cc6ac3b7f71db38 100644
--- a/src/components/AudioViewer.js
+++ b/src/components/AudioViewer.js
@@ -6,16 +6,23 @@ export class AudioViewer extends Component {
   /* eslint-disable jsx-a11y/media-has-caption */
   /** */
   render() {
-    const { classes, audioResources } = this.props;
+    const {
+      captions, classes, audioOptions, audioResources,
+    } = this.props;
 
     return (
       <div className={classes.container}>
-        <audio controls className={classes.audio}>
+        <audio className={classes.audio} {...audioOptions}>
           {audioResources.map(audio => (
             <Fragment key={audio.id}>
               <source src={audio.id} type={audio.getFormat()} />
             </Fragment>
           ))}
+          {captions.map(caption => (
+            <Fragment key={caption.id}>
+              <track src={caption.id} label={caption.getDefaultLabel()} srcLang={caption.getProperty('language')} />
+            </Fragment>
+          ))}
         </audio>
       </div>
     );
@@ -24,10 +31,14 @@ export class AudioViewer extends Component {
 }
 
 AudioViewer.propTypes = {
+  audioOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
   audioResources: PropTypes.arrayOf(PropTypes.object),
+  captions: PropTypes.arrayOf(PropTypes.object),
   classes: PropTypes.objectOf(PropTypes.string).isRequired,
 };
 
 AudioViewer.defaultProps = {
+  audioOptions: {},
   audioResources: [],
+  captions: [],
 };
diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js
index 23150c6a39414a3d8026902e74f3e259677ad44a..137a4ef0bcf3f1ed42a78e90ef5df08073752b2e 100644
--- a/src/components/CanvasAnnotations.js
+++ b/src/components/CanvasAnnotations.js
@@ -7,6 +7,7 @@ import MenuItem from '@material-ui/core/MenuItem';
 import ListItemText from '@material-ui/core/ListItemText';
 import Typography from '@material-ui/core/Typography';
 import SanitizedHtml from '../containers/SanitizedHtml';
+import { ScrollTo } from './ScrollTo';
 
 /**
  * CanvasAnnotations ~
@@ -59,6 +60,7 @@ export class CanvasAnnotations extends Component {
     const {
       annotations, classes, index, label, selectedAnnotationId, t, totalSize,
       listContainerComponent, htmlSanitizationRuleSet, hoveredAnnotationIds,
+      containerRef,
     } = this.props;
     if (annotations.length === 0) return <></>;
 
@@ -70,38 +72,45 @@ export class CanvasAnnotations extends Component {
         <MenuList autoFocusItem variant="selectedMenu">
           {
             annotations.map(annotation => (
-              <MenuItem
-                button
-                component={listContainerComponent}
-                className={clsx(
-                  classes.annotationListItem,
-                  {
-                    [classes.hovered]: hoveredAnnotationIds.includes(annotation.id),
-                  },
-                )}
-                key={annotation.id}
-                annotationid={annotation.id}
-                selected={selectedAnnotationId === annotation.id}
-                onClick={e => this.handleClick(e, annotation)}
-                onFocus={() => this.handleAnnotationHover(annotation)}
-                onBlur={this.handleAnnotationBlur}
-                onMouseEnter={() => this.handleAnnotationHover(annotation)}
-                onMouseLeave={this.handleAnnotationBlur}
+              <ScrollTo
+                containerRef={containerRef}
+                key={`${annotation.id}-scroll`}
+                offsetTop={96} // offset for the height of the form above
+                scrollTo={selectedAnnotationId === annotation.id}
               >
-                <ListItemText primaryTypographyProps={{ variant: 'body2' }}>
-                  <SanitizedHtml
-                    ruleSet={htmlSanitizationRuleSet}
-                    htmlString={annotation.content}
-                  />
-                  <div>
+                <MenuItem
+                  button
+                  component={listContainerComponent}
+                  className={clsx(
+                    classes.annotationListItem,
                     {
-                      annotation.tags.map(tag => (
-                        <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} key={tag.toString()} />
-                      ))
-                    }
-                  </div>
-                </ListItemText>
-              </MenuItem>
+                      [classes.hovered]: hoveredAnnotationIds.includes(annotation.id),
+                    },
+                  )}
+                  key={annotation.id}
+                  annotationid={annotation.id}
+                  selected={selectedAnnotationId === annotation.id}
+                  onClick={e => this.handleClick(e, annotation)}
+                  onFocus={() => this.handleAnnotationHover(annotation)}
+                  onBlur={this.handleAnnotationBlur}
+                  onMouseEnter={() => this.handleAnnotationHover(annotation)}
+                  onMouseLeave={this.handleAnnotationBlur}
+                >
+                  <ListItemText primaryTypographyProps={{ variant: 'body2' }}>
+                    <SanitizedHtml
+                      ruleSet={htmlSanitizationRuleSet}
+                      htmlString={annotation.content}
+                    />
+                    <div>
+                      {
+                        annotation.tags.map(tag => (
+                          <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} key={tag.toString()} />
+                        ))
+                      }
+                    </div>
+                  </ListItemText>
+                </MenuItem>
+              </ScrollTo>
             ))
           }
         </MenuList>
@@ -118,6 +127,10 @@ CanvasAnnotations.propTypes = {
     }),
   ),
   classes: PropTypes.objectOf(PropTypes.string),
+  containerRef: PropTypes.oneOfType([
+    PropTypes.func,
+    PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
+  ]),
   deselectAnnotation: PropTypes.func.isRequired,
   hoverAnnotation: PropTypes.func.isRequired,
   hoveredAnnotationIds: PropTypes.arrayOf(PropTypes.string),
@@ -134,6 +147,7 @@ CanvasAnnotations.propTypes = {
 CanvasAnnotations.defaultProps = {
   annotations: [],
   classes: {},
+  containerRef: undefined,
   hoveredAnnotationIds: [],
   htmlSanitizationRuleSet: 'iiif',
   listContainerComponent: 'li',
diff --git a/src/components/CollapsibleSection.js b/src/components/CollapsibleSection.js
index e5e0f5337ecc30526ba997485b3772c412d078b9..eca0a2ff33d747c7e6782acebd50774c9de49b3b 100644
--- a/src/components/CollapsibleSection.js
+++ b/src/components/CollapsibleSection.js
@@ -49,7 +49,7 @@ export class CollapsibleSection extends Component {
             aria-label={
               t(
                 open ? 'collapseSection' : 'expandSection',
-                { section: label.toLowerCase() },
+                { section: label },
               )
             }
             aria-expanded={open}
diff --git a/src/components/CollectionDialog.js b/src/components/CollectionDialog.js
index f66e36a48cda54890294652bb2fdbcc51ea8a4ca..53e65d67117f2b899e2535677041313eb3fedd71 100644
--- a/src/components/CollectionDialog.js
+++ b/src/components/CollectionDialog.js
@@ -13,21 +13,12 @@ import {
 } from '@material-ui/core';
 import ArrowBackIcon from '@material-ui/icons/ArrowBackSharp';
 import Skeleton from '@material-ui/lab/Skeleton';
+import asArray from '../lib/asArray';
 import { LabelValueMetadata } from './LabelValueMetadata';
 import CollapsibleSection from '../containers/CollapsibleSection';
 import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent';
 import ManifestInfo from '../containers/ManifestInfo';
 
-/**
- */
-function asArray(value) {
-  if (!Array.isArray(value)) {
-    return [value];
-  }
-
-  return value;
-}
-
 /**
  * a dialog providing the possibility to select the collection
  */
@@ -243,7 +234,11 @@ export class CollectionDialog extends Component {
             <MenuList>
               {
                 collections.map(c => (
-                  <MenuItem key={c.id} onClick={() => { this.selectCollection(c); }}>
+                  <MenuItem
+                    key={c.id}
+                    onClick={() => { this.selectCollection(c); }}
+                    className={classes.collectionItem}
+                  >
                     {CollectionDialog.getUseableLabel(c)}
                   </MenuItem>
                 ))
@@ -254,7 +249,11 @@ export class CollectionDialog extends Component {
             <MenuList>
               {
                 manifest.getManifests().map(m => (
-                  <MenuItem key={m.id} onClick={() => { this.selectManifest(m); }}>
+                  <MenuItem
+                    key={m.id}
+                    onClick={() => { this.selectManifest(m); }}
+                    className={classes.collectionItem}
+                  >
                     {CollectionDialog.getUseableLabel(m)}
                   </MenuItem>
                 ))
diff --git a/src/components/IIIFThumbnail.js b/src/components/IIIFThumbnail.js
index 174e9af0bb922b4441b194df073241420c1f22db..c942f4824231739d722ab08c9682abdc3165148f 100644
--- a/src/components/IIIFThumbnail.js
+++ b/src/components/IIIFThumbnail.js
@@ -98,6 +98,12 @@ export class IIIFThumbnail extends Component {
       styleProps.height = maxHeight;
     } else if (!thumbHeight && thumbWidth) {
       styleProps.width = maxWidth;
+    } else {
+      // The thumbnail wasn't retrieved via an Image API service,
+      // and its dimensions are not specified in the JSON-LD
+      // (note that this may result in a blurry image)
+      styleProps.width = maxWidth;
+      styleProps.height = maxHeight;
     }
 
     return {
@@ -109,12 +115,12 @@ export class IIIFThumbnail extends Component {
   /** */
   image() {
     const {
-      thumbnail, resource, maxHeight, maxWidth,
+      thumbnail, resource, maxHeight, maxWidth, thumbnailsConfig,
     } = this.props;
 
     if (thumbnail) return thumbnail;
 
-    const image = getThumbnail(resource, { maxHeight, maxWidth });
+    const image = getThumbnail(resource, { ...thumbnailsConfig, maxHeight, maxWidth });
 
     if (image && image.url) return image;
 
@@ -183,6 +189,7 @@ IIIFThumbnail.propTypes = {
     url: PropTypes.string.isRequired,
     width: PropTypes.number,
   }),
+  thumbnailsConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types
   variant: PropTypes.oneOf(['inside', 'outside']),
 };
 
@@ -197,5 +204,6 @@ IIIFThumbnail.defaultProps = {
   maxWidth: null,
   style: {},
   thumbnail: null,
+  thumbnailsConfig: {},
   variant: null,
 };
diff --git a/src/components/ManifestForm.js b/src/components/ManifestForm.js
index c4e03ec51ec756c26c5ada8b82489bdbdf63d626..19702f071430b2d07f6f29889e811a012ae056d6 100644
--- a/src/components/ManifestForm.js
+++ b/src/components/ManifestForm.js
@@ -18,24 +18,11 @@ export class ManifestForm extends Component {
       formValue: '',
     };
 
-    this.inputRef = React.createRef();
     this.formSubmit = this.formSubmit.bind(this);
     this.handleCancel = this.handleCancel.bind(this);
     this.handleInputChange = this.handleInputChange.bind(this);
   }
 
-  /**
-   *
-   * @param {*} prevProps
-   * @param {*} prevState
-   */
-  componentDidUpdate() {
-    const { addResourcesOpen } = this.props;
-    if (this.inputRef && this.inputRef.current && addResourcesOpen) {
-      this.inputRef.current.focus();
-    }
-  }
-
   /**
    * Reset the form state
    */
@@ -80,16 +67,19 @@ export class ManifestForm extends Component {
   render() {
     const { formValue } = this.state;
     const {
+      addResourcesOpen,
       classes,
       onCancel,
       t,
     } = this.props;
+    if (!addResourcesOpen) return null;
+
     return (
       <form onSubmit={this.formSubmit}>
         <Grid container spacing={2}>
           <Grid item xs={12} sm={8} md={9}>
             <TextField
-              inputRef={this.inputRef}
+              autoFocus
               fullWidth
               value={formValue}
               id="manifestURL"
diff --git a/src/components/ManifestListItem.js b/src/components/ManifestListItem.js
index 12f8516d2624005235febfce05cab80ae5bea972..edf14971b5ccc6f7115e67a7b92daebb2768ca50 100644
--- a/src/components/ManifestListItem.js
+++ b/src/components/ManifestListItem.js
@@ -104,21 +104,25 @@ export class ManifestListItem extends React.Component {
               >
                 <Grid container spacing={2} className={classes.label} component="span">
                   <Grid item xs={4} sm={3} component="span">
-                    <Img
-                      className={[classes.thumbnail, ns('manifest-list-item-thumb')].join(' ')}
-                      src={[thumbnail]}
-                      alt=""
-                      height="80"
-                      unloader={(
-                        <Skeleton
-                          variant="rect"
-                          animation={false}
-                          className={classes.placeholder}
-                          height={80}
-                          width={120}
+                    { thumbnail
+                      ? (
+                        <Img
+                          className={[classes.thumbnail, ns('manifest-list-item-thumb')].join(' ')}
+                          src={[thumbnail]}
+                          alt=""
+                          height="80"
+                          unloader={(
+                            <Skeleton
+                              variant="rect"
+                              animation={false}
+                              className={classes.placeholder}
+                              height={80}
+                              width={120}
+                            />
+                          )}
                         />
-                      )}
-                    />
+                      )
+                      : <Skeleton className={classes.placeholder} variant="rect" height={80} width={120} />}
                   </Grid>
                   <Grid item xs={8} sm={9} component="span">
                     { isCollection && (
@@ -134,26 +138,29 @@ export class ManifestListItem extends React.Component {
               </ButtonBase>
             </Grid>
             <Grid item xs={8} sm={4}>
-              <Typography className={ns('manifest-list-item-provider')}>{provider || t('addedFromUrl')}</Typography>
-              <Typography>{t('numItems', { number: size })}</Typography>
+              <Typography className={ns('manifest-list-item-provider')}>{provider}</Typography>
+              <Typography>{t('numItems', { count: size, number: size })}</Typography>
             </Grid>
 
             <Grid item xs={4} sm={2}>
-              <Img
-                src={[manifestLogo]}
-                alt=""
-                role="presentation"
-                className={classes.logo}
-                unloader={(
-                  <Skeleton
-                    variant="rect"
-                    animation={false}
-                    className={classes.placeholder}
-                    height={60}
-                    width={60}
-                  />
+              { manifestLogo
+                && (
+                <Img
+                  src={[manifestLogo]}
+                  alt=""
+                  role="presentation"
+                  className={classes.logo}
+                  unloader={(
+                    <Skeleton
+                      variant="rect"
+                      animation={false}
+                      className={classes.placeholder}
+                      height={60}
+                      width={60}
+                    />
+                  )}
+                />
                 )}
-              />
             </Grid>
           </Grid>
         ) : (
diff --git a/src/components/ManifestRelatedLinks.js b/src/components/ManifestRelatedLinks.js
index 50d034e7b385700db1ec7e6038c102b315dc993f..533b4522d1b408ff62173ee5e964893651b50841 100644
--- a/src/components/ManifestRelatedLinks.js
+++ b/src/components/ManifestRelatedLinks.js
@@ -78,7 +78,7 @@ export class ManifestRelatedLinks extends Component {
                       {related.label || related.value}
                     </Link>
                     { related.format && (
-                      <Typography component="span">{`(${related.format})`}</Typography>
+                      <Typography component="span">{` (${related.format})`}</Typography>
                     )}
                   </Typography>
                 ))
diff --git a/src/components/PrimaryWindow.js b/src/components/PrimaryWindow.js
index 852fef9f880c3194ff0ce022787f9d87bbc957a9..ee5d4e672c07edeaade90ed46aecd6058bca9661 100644
--- a/src/components/PrimaryWindow.js
+++ b/src/components/PrimaryWindow.js
@@ -75,14 +75,16 @@ export class PrimaryWindow extends Component {
    * Render the component
    */
   render() {
-    const { isCollectionDialogVisible, windowId, classes } = this.props;
+    const {
+      isCollectionDialogVisible, windowId, classes, children,
+    } = this.props;
     return (
       <div className={classNames(ns('primary-window'), classes.primaryWindow)}>
         <WindowSideBar windowId={windowId} />
         <CompanionArea windowId={windowId} position="left" />
         { isCollectionDialogVisible && <CollectionDialog windowId={windowId} /> }
         <Suspense fallback={<div />}>
-          {this.renderViewer()}
+          {children || this.renderViewer()}
         </Suspense>
       </div>
     );
@@ -91,6 +93,7 @@ export class PrimaryWindow extends Component {
 
 PrimaryWindow.propTypes = {
   audioResources: PropTypes.arrayOf(PropTypes.object),
+  children: PropTypes.node,
   classes: PropTypes.objectOf(PropTypes.string).isRequired,
   isCollection: PropTypes.bool,
   isCollectionDialogVisible: PropTypes.bool,
@@ -102,6 +105,7 @@ PrimaryWindow.propTypes = {
 
 PrimaryWindow.defaultProps = {
   audioResources: [],
+  children: undefined,
   isCollection: false,
   isCollectionDialogVisible: false,
   isFetching: false,
diff --git a/src/components/SearchPanelNavigation.js b/src/components/SearchPanelNavigation.js
index 8a44237a8f7736f930acdc8e6f34937cefb4c8b9..59f9eb65b79d41e5e935791658c0ab5ab0106fcc 100644
--- a/src/components/SearchPanelNavigation.js
+++ b/src/components/SearchPanelNavigation.js
@@ -42,13 +42,17 @@ export class SearchPanelNavigation extends Component {
   */
   render() {
     const {
-      searchHits, selectedContentSearchAnnotation, classes, t, direction,
+      numTotal, searchHits, selectedContentSearchAnnotation, classes, t, direction,
     } = this.props;
 
     const iconStyle = direction === 'rtl' ? { transform: 'rotate(180deg)' } : {};
 
     const currentHitIndex = searchHits
       .findIndex(val => val.annotations.includes(selectedContentSearchAnnotation[0]));
+    let lengthText = searchHits.length;
+    if (searchHits.length < numTotal) {
+      lengthText += '+';
+    }
     return (
       <>
         {(searchHits.length > 0) && (
@@ -61,7 +65,7 @@ export class SearchPanelNavigation extends Component {
               <ChevronLeftIcon style={iconStyle} />
             </MiradorMenuButton>
             <span style={{ unicodeBidi: 'plaintext' }}>
-              {t('pagination', { current: currentHitIndex + 1, total: searchHits.length })}
+              {t('pagination', { current: currentHitIndex + 1, total: lengthText })}
             </span>
             <MiradorMenuButton
               aria-label={t('searchNextResult')}
@@ -79,6 +83,7 @@ export class SearchPanelNavigation extends Component {
 SearchPanelNavigation.propTypes = {
   classes: PropTypes.objectOf(PropTypes.string),
   direction: PropTypes.string.isRequired,
+  numTotal: PropTypes.number,
   searchHits: PropTypes.arrayOf(PropTypes.object),
   searchService: PropTypes.shape({
     id: PropTypes.string,
@@ -90,6 +95,7 @@ SearchPanelNavigation.propTypes = {
 };
 SearchPanelNavigation.defaultProps = {
   classes: {},
+  numTotal: undefined,
   searchHits: [],
   t: key => key,
 };
diff --git a/src/components/SearchResults.js b/src/components/SearchResults.js
index 23365db543d4dda44a0ef5ba1228d069bb8270e8..7c5ca415b3d3d993451d6b3c8c0044358c5f7c4f 100644
--- a/src/components/SearchResults.js
+++ b/src/components/SearchResults.js
@@ -89,6 +89,7 @@ export class SearchResults extends Component {
       query,
       searchAnnotations,
       searchHits,
+      searchNumTotal,
       t,
       windowId,
     } = this.props;
@@ -122,8 +123,14 @@ export class SearchResults extends Component {
           </LiveMessenger>
         </List>
         { nextSearch && (
-          <Button color="secondary" onClick={() => fetchSearch(windowId, companionWindowId, nextSearch, query)}>
+          <Button
+            className={classes.moreButton}
+            color="secondary"
+            onClick={() => fetchSearch(windowId, companionWindowId, nextSearch, query)}
+          >
             {t('moreResults')}
+            <br />
+            {`(${t('searchResultsRemaining', { numLeft: searchNumTotal - searchHits.length })})`}
           </Button>
         )}
       </>
@@ -144,6 +151,7 @@ SearchResults.propTypes = {
   query: PropTypes.string,
   searchAnnotations: PropTypes.arrayOf(PropTypes.object),
   searchHits: PropTypes.arrayOf(PropTypes.object),
+  searchNumTotal: PropTypes.number,
   t: PropTypes.func,
   windowId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
 };
@@ -156,5 +164,6 @@ SearchResults.defaultProps = {
   query: undefined,
   searchAnnotations: [],
   searchHits: [],
+  searchNumTotal: undefined,
   t: k => k,
 };
diff --git a/src/components/ThumbnailCanvasGrouping.js b/src/components/ThumbnailCanvasGrouping.js
index cc966ff815eba1093a823238a750ce70f5ac309c..72f08bb4a3479bf7060b5ed174952e8eca3cefe8 100644
--- a/src/components/ThumbnailCanvasGrouping.js
+++ b/src/components/ThumbnailCanvasGrouping.js
@@ -44,7 +44,7 @@ export class ThumbnailCanvasGrouping extends PureComponent {
           ...style,
           boxSizing: 'content-box',
           height: (Number.isInteger(style.height)) ? style.height - SPACING : null,
-          left: style.left + SPACING,
+          left: (Number.isInteger(style.left)) ? style.left + SPACING : null,
           top: style.top + SPACING,
           width: (Number.isInteger(style.width)) ? style.width - SPACING : null,
         }}
diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js
index d887f8bf94143389fd6566b4ac6d7a5c7dd123b6..fb85a483815b11b45d68ddccb007814fd9f54915 100644
--- a/src/components/ThumbnailNavigation.js
+++ b/src/components/ThumbnailNavigation.js
@@ -69,7 +69,7 @@ export class ThumbnailNavigation extends Component {
    */
   calculateScaledSize(index) {
     const { thumbnailNavigation, canvasGroupings, position } = this.props;
-    const canvases = canvasGroupings[index];
+    const canvases = canvasGroupings[index] || [];
     const world = new CanvasWorld(canvases);
     const bounds = world.worldBounds();
     switch (position) {
diff --git a/src/components/VideoViewer.js b/src/components/VideoViewer.js
index 31810a21c86cb3e5898829f077409be78b6ed394..4c285507f1299632e70de74ea473c1b238f7d51d 100644
--- a/src/components/VideoViewer.js
+++ b/src/components/VideoViewer.js
@@ -1,6 +1,6 @@
 import flatten from 'lodash/flatten';
 import flattenDeep from 'lodash/flattenDeep';
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
 import PropTypes from 'prop-types';
 import AnnotationItem from '../lib/AnnotationItem';
 import AnnotationsOverlayVideo from '../containers/AnnotationsOverlayVideo';
@@ -85,7 +85,7 @@ export class VideoViewer extends Component {
   /** */
   render() {
     const {
-      canvas, classes, currentTime, windowId,
+      canvas, classes, currentTime, videoOptions, windowId,
     } = this.props;
 
     const videoResources = flatten(
@@ -119,7 +119,7 @@ export class VideoViewer extends Component {
         <div className={classes.flexFill}>
           { video && (
             <>
-              <video className={classes.video} key={video.id} ref={this.videoRef}>
+              <video className={classes.video} key={video.id} ref={this.videoRef} {...videoOptions}>
                 <source src={video.id} type={video.getFormat()} />
               </video>
               <AnnotationsOverlayVideo windowId={windowId} videoRef={this.videoRef} videoTarget={videoTargetTemporalfragment} key={`${windowId} ${video.id}`} />
@@ -141,6 +141,7 @@ VideoViewer.propTypes = {
   paused: PropTypes.bool,
   setCurrentTime: PropTypes.func,
   setPaused: PropTypes.func,
+  videoOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
   windowId: PropTypes.string.isRequired,
 };
 
@@ -151,4 +152,5 @@ VideoViewer.defaultProps = {
   paused: true,
   setCurrentTime: () => {},
   setPaused: () => {},
+  videoOptions: {},
 };
diff --git a/src/components/WindowSideBarAnnotationsPanel.js b/src/components/WindowSideBarAnnotationsPanel.js
index 06068abe31e400d2b9500238310850fdc64962ea..f5a13189296267a8fb9aa9acb0b6b43cf41ed87f 100644
--- a/src/components/WindowSideBarAnnotationsPanel.js
+++ b/src/components/WindowSideBarAnnotationsPanel.js
@@ -10,6 +10,13 @@ import ns from '../config/css-ns';
  * WindowSideBarAnnotationsPanel ~
 */
 export class WindowSideBarAnnotationsPanel extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.containerRef = React.createRef();
+  }
+
   /**
    * Returns the rendered component
   */
@@ -23,15 +30,18 @@ export class WindowSideBarAnnotationsPanel extends Component {
         paperClassName={ns('window-sidebar-annotation-panel')}
         windowId={windowId}
         id={id}
+        ref={this.containerRef}
+        otherRef={this.containerRef}
         titleControls={<AnnotationSettings windowId={windowId} />}
       >
         <div className={classes.section}>
-          <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { number: annotationCount })}</Typography>
+          <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography>
         </div>
 
         {canvasIds.map((canvasId, index) => (
           <CanvasAnnotations
             canvasId={canvasId}
+            containerRef={this.containerRef}
             key={canvasId}
             index={index}
             totalSize={canvasIds.length}
diff --git a/src/components/WindowSideBarCollectionPanel.js b/src/components/WindowSideBarCollectionPanel.js
index 78b6f2b0c93def01ae376b1375a11acbf6adff7e..f480ec42ca2bda9e07da7cf56283f0745e57a9e4 100644
--- a/src/components/WindowSideBarCollectionPanel.js
+++ b/src/components/WindowSideBarCollectionPanel.js
@@ -31,7 +31,7 @@ export class WindowSideBarCollectionPanel extends Component {
 
     const behaviors = collection.getProperty('behavior');
 
-    if (Array.isArray(behaviors)) return collection.includes('multi-part');
+    if (Array.isArray(behaviors)) return behaviors.includes('multi-part');
 
     return behaviors === 'multi-part';
   }
diff --git a/src/components/WorkspaceControlPanel.js b/src/components/WorkspaceControlPanel.js
index 8b7fc2443674cfe17b3b402891f93cb8f27b11fb..de849c8fb72e11fbc5c7fe0ec8e51996e684c35e 100644
--- a/src/components/WorkspaceControlPanel.js
+++ b/src/components/WorkspaceControlPanel.js
@@ -4,8 +4,7 @@ import classNames from 'classnames';
 import AppBar from '@material-ui/core/AppBar';
 import Toolbar from '@material-ui/core/Toolbar';
 import WorkspaceAddButton from '../containers/WorkspaceAddButton';
-import WorkspaceControlPanelButtons
-  from '../containers/WorkspaceControlPanelButtons';
+import WorkspaceControlPanelButtons from '../containers/WorkspaceControlPanelButtons';
 import Branding from '../containers/Branding';
 import ns from '../config/css-ns';
 
diff --git a/src/components/WorkspaceExport.js b/src/components/WorkspaceExport.js
index a9ce6ed290f8274d88a1912ad26e115a3b25d0bc..2a7ff7e0ce6a882742e5ee6ecb10ac30466d52ae 100644
--- a/src/components/WorkspaceExport.js
+++ b/src/components/WorkspaceExport.js
@@ -3,13 +3,17 @@ import Button from '@material-ui/core/Button';
 import Dialog from '@material-ui/core/Dialog';
 import DialogActions from '@material-ui/core/DialogActions';
 import DialogTitle from '@material-ui/core/DialogTitle';
+import DialogContent from '@material-ui/core/DialogContent';
 import Typography from '@material-ui/core/Typography';
 import Snackbar from '@material-ui/core/Snackbar';
 import IconButton from '@material-ui/core/IconButton';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import CloseIcon from '@material-ui/icons/Close';
+import Accordion from '@material-ui/core/Accordion';
+import AccordionSummary from '@material-ui/core/AccordionSummary';
+import AccordionDetails from '@material-ui/core/AccordionDetails';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent';
 
 /**
  */
@@ -50,7 +54,7 @@ export class WorkspaceExport extends Component {
    */
   render() {
     const {
-      children, container, open, t,
+      children, classes, container, open, t,
     } = this.props;
     const { copied } = this.state;
 
@@ -87,12 +91,24 @@ export class WorkspaceExport extends Component {
         <DialogTitle id="form-dialog-title" disableTypography>
           <Typography variant="h2">{t('downloadExport')}</Typography>
         </DialogTitle>
-        <ScrollIndicatedDialogContent>
-          {children}
-          <pre>
-            {this.exportedState()}
-          </pre>
-        </ScrollIndicatedDialogContent>
+
+        <DialogContent>
+          <Accordion elevation={0}>
+            <AccordionSummary
+              classes={{ root: classes.accordionTitle }}
+              expandIcon={<ExpandMoreIcon />}
+            >
+              <Typography variant="h4">{t('viewWorkspaceConfiguration')}</Typography>
+            </AccordionSummary>
+            <AccordionDetails>
+              {children}
+              <pre>
+                {this.exportedState()}
+              </pre>
+            </AccordionDetails>
+          </Accordion>
+        </DialogContent>
+
         <DialogActions>
           <Button onClick={this.handleClose}>{t('cancel')}</Button>
           <CopyToClipboard
@@ -109,6 +125,7 @@ export class WorkspaceExport extends Component {
 
 WorkspaceExport.propTypes = {
   children: PropTypes.node,
+  classes: PropTypes.objectOf(PropTypes.string),
   container: PropTypes.object, // eslint-disable-line react/forbid-prop-types
   exportableState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   handleClose: PropTypes.func.isRequired,
@@ -118,6 +135,7 @@ WorkspaceExport.propTypes = {
 
 WorkspaceExport.defaultProps = {
   children: null,
+  classes: {},
   container: null,
   open: false,
   t: key => key,
diff --git a/src/config/settings.js b/src/config/settings.js
index 414de473472616592b29ea6610e8c86200c010e7..50e2bc752798c25001332572c1e6826c66260dab 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -4,7 +4,7 @@ export default {
   state: {
     // slice: 'mirador' // Configure the top-level slice of state for mirador selectors
   },
-  canvasNavigation: { // Set the hight and width of canvas thumbnails in the  CanvasNavigation companion window
+  canvasNavigation: { // Set the height and width of canvas thumbnails in the  CanvasNavigation companion window
     height: 50,
     width: 50,
   },
@@ -222,13 +222,18 @@ export default {
     en: 'English',
     fr: 'Français',
     ja: '日本語',
+    kr: '한국어',
     lt: 'Lietuvių',
     nl: 'Nederlands',
+    'nb-NO': 'Norwegian Bokmål',
+    pl: 'Polski',
     'pt-BR': 'Português do Brasil',
+    vi:'Tiếng Việt',
     'zh-CN': '中文(简体)',
     'zh-TW': '中文(繁體)',
     it: "Italiano",
     sr: 'Српски',
+    sv: 'Svenska'
   },
   annotations: {
     htmlSanitizationRuleSet: 'iiif', // See src/lib/htmlRules.js for acceptable values
@@ -268,13 +273,14 @@ export default {
     highlightAllAnnotations: false, // Configure whether to display annotations on the canvas by default
     showLocalePicker: false, // Configure locale picker for multi-lingual metadata
     sideBarOpen: false, // Configure if the sidebar (and its content panel) is open by default
+    switchCanvasOnSearch: true, // Configure if Mirador should automatically switch to the canvas of the first search result
     panels: { // Configure which panels are visible in WindowSideBarButtons
       info: true,
       attribution: true,
       canvas: true,
       annotations: true,
       search: true,
-      layers: false,
+      layers: true,
     },
     views: [
       { key: 'single', behaviors: ['individuals'] },
@@ -282,6 +288,10 @@ export default {
       { key: 'scroll', behaviors: ['continuous'] },
       { key: 'gallery' },
     ],
+    elastic: {
+      height: 400,
+      width: 480
+    }
   },
   windows: [ // Array of windows to be open when mirador initializes (each object should at least provide a manifestId key with the value of the IIIF presentation manifest to load)
     /**
@@ -295,6 +305,9 @@ export default {
     // ../lib/MiradorViewer.js `windowAction`
     */
   ],
+  thumbnails: {
+    preferredFormats: ['jpg', 'png', 'webp', 'tif'],
+  },
   thumbnailNavigation: {
     defaultPosition: 'off', // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right"
     displaySettings: true, // Display the settings for this in WindowTopMenu
@@ -342,6 +355,14 @@ export default {
     windows: true,
     workspace: true,
   },
+  audioOptions: { // Additional props passed to <audio> element
+    controls: true,
+    crossOrigin: 'anonymous',
+  },
+  videoOptions: { // Additional props passed to <audio> element
+    controls: true,
+    crossOrigin: 'anonymous',
+  },
   auth: {
     serviceProfiles: [
       { profile: 'http://iiif.io/api/auth/1/external', external: true },
diff --git a/src/containers/AudioViewer.js b/src/containers/AudioViewer.js
index 65d198f5ad1be542536bb025064a6c25810c418e..e422c6d6b6e00458773de3c365080307590b9fd9 100644
--- a/src/containers/AudioViewer.js
+++ b/src/containers/AudioViewer.js
@@ -4,12 +4,14 @@ import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core';
 import { withPlugins } from '../extend/withPlugins';
 import { AudioViewer } from '../components/AudioViewer';
-import { getVisibleCanvasAudioResources } from '../state/selectors';
+import { getConfig, getVisibleCanvasAudioResources, getVisibleCanvasCaptions } from '../state/selectors';
 
 /** */
 const mapStateToProps = (state, { windowId }) => (
   {
+    audioOptions: getConfig(state).audioOptions,
     audioResources: getVisibleCanvasAudioResources(state, { windowId }) || [],
+    captions: getVisibleCanvasCaptions(state, { windowId }) || [],
   }
 );
 
diff --git a/src/containers/CollectionDialog.js b/src/containers/CollectionDialog.js
index d663a90fa87a820e398fbe47bd5f248898a12d85..22210c1658156664525855c1142616563c5ce79b 100644
--- a/src/containers/CollectionDialog.js
+++ b/src/containers/CollectionDialog.js
@@ -54,6 +54,9 @@ const styles = theme => ({
     padding: '16px',
     paddingTop: 0,
   },
+  collectionItem: {
+    whiteSpace: 'normal',
+  },
   collectionMetadata: {
     padding: '16px',
   },
@@ -64,7 +67,7 @@ const styles = theme => ({
     position: 'absolute !important',
   },
   dialogContent: {
-    padding: 0,
+    padding: theme.spacing(1),
   },
   light: {
     color: theme.palette.grey[400],
diff --git a/src/containers/CustomPanel.js b/src/containers/CustomPanel.js
index c9dfa5dec5ad5ae2320f331132974eb51afeda4e..493ad146335bc1e55955512e721ad1bd83244ed4 100644
--- a/src/containers/CustomPanel.js
+++ b/src/containers/CustomPanel.js
@@ -4,8 +4,6 @@ import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
 import { CustomPanel } from '../components/CustomPanel';
-import {
-} from '../state/selectors';
 
 /**
  * mapStateToProps - to hook up connect
diff --git a/src/containers/IIIFAuthentication.js b/src/containers/IIIFAuthentication.js
index ddc37415752a29259249f3356abb9e57acdd7134..7d51087168d91d51425c07b196e6e911ba37a3b2 100644
--- a/src/containers/IIIFAuthentication.js
+++ b/src/containers/IIIFAuthentication.js
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { compose } from 'redux';
 import { withTranslation } from 'react-i18next';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
 import {
diff --git a/src/containers/IIIFThumbnail.js b/src/containers/IIIFThumbnail.js
index 71c95d0a4900ee5b46b177743c6952356b6e5644..c596b508cd92fee25b69b5e09e198011182363e7 100644
--- a/src/containers/IIIFThumbnail.js
+++ b/src/containers/IIIFThumbnail.js
@@ -1,9 +1,21 @@
 import { compose } from 'redux';
+import { connect } from 'react-redux';
 import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
+import {
+  getConfig,
+} from '../state/selectors';
 import { IIIFThumbnail } from '../components/IIIFThumbnail';
 
+/**
+ * mapStateToProps - to hook up connect
+ * @private
+ */
+const mapStateToProps = (state) => ({
+  thumbnailsConfig: getConfig(state).thumbnails,
+});
+
 /**
  * Styles for withStyles HOC
  */
@@ -50,6 +62,7 @@ const styles = theme => ({
 const enhance = compose(
   withStyles(styles),
   withTranslation(),
+  connect(mapStateToProps),
   withPlugins('IIIFThumbnail'),
 );
 
diff --git a/src/containers/SearchPanelNavigation.js b/src/containers/SearchPanelNavigation.js
index 2202addf23231006a747920cf0ffc1771d6b2d3d..614b2419209114973c256fc4029944724185b715 100644
--- a/src/containers/SearchPanelNavigation.js
+++ b/src/containers/SearchPanelNavigation.js
@@ -7,6 +7,7 @@ import { SearchPanelNavigation } from '../components/SearchPanelNavigation';
 import * as actions from '../state/actions';
 import {
   getSelectedContentSearchAnnotationIds,
+  getSearchNumTotal,
   getSortedSearchHitsForCompanionWindow,
   getThemeDirection,
 } from '../state/selectors';
@@ -18,6 +19,7 @@ import {
  */
 const mapStateToProps = (state, { companionWindowId, windowId }) => ({
   direction: getThemeDirection(state),
+  numTotal: getSearchNumTotal(state, { companionWindowId, windowId }),
   searchHits: getSortedSearchHitsForCompanionWindow(state, { companionWindowId, windowId }),
   selectedContentSearchAnnotation: getSelectedContentSearchAnnotationIds(state, {
     companionWindowId, windowId,
diff --git a/src/containers/SearchResults.js b/src/containers/SearchResults.js
index 8f451a3a3a6421ab59ebaa69a1e7ddea7f265dd2..15e660d99a297bfdb9a13a57d4f19e48697c8097 100644
--- a/src/containers/SearchResults.js
+++ b/src/containers/SearchResults.js
@@ -9,6 +9,7 @@ import {
   getNextSearchId,
   getSearchQuery,
   getSearchIsFetching,
+  getSearchNumTotal,
   getSortedSearchHitsForCompanionWindow,
   getSortedSearchAnnotationsForCompanionWindow,
 } from '../state/selectors';
@@ -25,6 +26,7 @@ const mapStateToProps = (state, { companionWindowId, windowId }) => ({
   searchAnnotations:
     getSortedSearchAnnotationsForCompanionWindow(state, { companionWindowId, windowId }),
   searchHits: getSortedSearchHitsForCompanionWindow(state, { companionWindowId, windowId }),
+  searchNumTotal: getSearchNumTotal(state, { companionWindowId, windowId }),
 });
 
 const mapDispatchToProps = {
@@ -33,6 +35,9 @@ const mapDispatchToProps = {
 
 /** */
 const styles = theme => ({
+  moreButton: {
+    width: '100%',
+  },
   navigation: {
     textTransform: 'none',
   },
diff --git a/src/containers/VideoViewer.js b/src/containers/VideoViewer.js
index d75b83509c093b8b7470ded254cb951084683d93..bbd148c854fb53e0ad20a9fe6734f66683af6ffe 100644
--- a/src/containers/VideoViewer.js
+++ b/src/containers/VideoViewer.js
@@ -6,6 +6,7 @@ import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
 import { VideoViewer } from '../components/VideoViewer';
 import {
+  getConfig,
   getCurrentCanvas,
   getCurrentCanvasWorld,
   getWindowMutedStatus,
@@ -20,6 +21,7 @@ const mapStateToProps = (state, { windowId }) => ({
   currentTime: getWindowCurrentTime(state, { windowId }),
   muted: getWindowMutedStatus(state, { windowId }),
   paused: getWindowPausedStatus(state, { windowId }),
+  videoOptions: getConfig(state).videoOptions,
 });
 
 /** */
diff --git a/src/containers/WindowTopMenuButton.js b/src/containers/WindowTopMenuButton.js
index dfd9a1a66040d0b78ab5a056f05138f5c48b2aac..54d863e2b688ca032ac5ed7af9e093e176b7fc18 100644
--- a/src/containers/WindowTopMenuButton.js
+++ b/src/containers/WindowTopMenuButton.js
@@ -18,7 +18,7 @@ const styles = theme => ({
 const enhance = compose(
   withTranslation(),
   withStyles(styles),
-  withPlugins('WindowTopMenuButtons'),
+  withPlugins('WindowTopMenuButton'),
 );
 
 export default enhance(WindowTopMenuButton);
diff --git a/src/containers/WorkspaceControlPanelButtons.js b/src/containers/WorkspaceControlPanelButtons.js
index 28f912a226f0da3b45d357421a3b5746c06a20c1..eaef08bd775196782632a0bb6022759aa41b3d57 100644
--- a/src/containers/WorkspaceControlPanelButtons.js
+++ b/src/containers/WorkspaceControlPanelButtons.js
@@ -1,8 +1,7 @@
 import { compose } from 'redux';
 import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
-import { WorkspaceControlPanelButtons }
-  from '../components/WorkspaceControlPanelButtons';
+import { WorkspaceControlPanelButtons } from '../components/WorkspaceControlPanelButtons';
 
 /**
  *
diff --git a/src/containers/WorkspaceExport.js b/src/containers/WorkspaceExport.js
index 069a52542815f19231609bdfe7e0f81997efbff7..e1ac982c10aa03c0cff9451dd1c3fbb63bac98ab 100644
--- a/src/containers/WorkspaceExport.js
+++ b/src/containers/WorkspaceExport.js
@@ -1,5 +1,6 @@
 import { compose } from 'redux';
 import { connect } from 'react-redux';
+import { withStyles } from '@material-ui/core/styles';
 import { withTranslation } from 'react-i18next';
 import { withPlugins } from '../extend/withPlugins';
 import { WorkspaceExport } from '../components/WorkspaceExport';
@@ -16,8 +17,18 @@ const mapStateToProps = state => ({
   exportableState: getExportableState(state),
 });
 
+/**
+ * Styles for the withStyles HOC
+ */
+const styles = theme => ({
+  accordionTitle: {
+    padding: 0,
+  },
+});
+
 const enhance = compose(
   withTranslation(),
+  withStyles(styles),
   connect(mapStateToProps, {}),
   withPlugins('WorkspaceExport'),
 );
diff --git a/src/extend/PluginProvider.js b/src/extend/PluginProvider.js
index ee9269e58f462c7a7500be9c946bd698d2604e0b..784191184315d514b59b25a64e23bd1897bc9f1b 100644
--- a/src/extend/PluginProvider.js
+++ b/src/extend/PluginProvider.js
@@ -16,7 +16,7 @@ export default function PluginProvider(props) {
     const connectedPlugins = connectPluginsToStore(plugins);
     addPluginsToCompanionWindowsRegistry(connectedPlugins);
     setPluginMap(createTargetToPluginMapping(connectedPlugins));
-  }, []);
+  }, [plugins]);
 
   return (
     <PluginContext.Provider value={pluginMap}>
diff --git a/src/extend/withPlugins.js b/src/extend/withPlugins.js
index b59530b83e9c41389945de3c94074925eff5d081..11320b1b737d222e4aa96ea20edec79de2cf50a9 100644
--- a/src/extend/withPlugins.js
+++ b/src/extend/withPlugins.js
@@ -43,7 +43,8 @@ function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-u
       );
     };
 
-    return plugins.wrap.reverse().reduce(pluginWrapper, <TargetComponent {...passDownProps} />);
+    return plugins.wrap.slice().reverse()
+      .reduce(pluginWrapper, <TargetComponent {...passDownProps} />);
   }
   const whatever = React.forwardRef(PluginHoc);
 
diff --git a/src/i18n.js b/src/i18n.js
index 3e96468d77b6f08ca2ab3e032d4853aa50eaf280..34f07f673713e0de8cee55b4a169ed9c0d34a844 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -7,14 +7,21 @@ import zhCn from './locales/zhCn/translation.json';
 import zhTw from './locales/zhTw/translation.json';
 import fr from './locales/fr/translation.json';
 import ja from './locales/ja/translation.json';
+import kr from './locales/kr/translation.json';
 import nl from './locales/nl/translation.json';
+import pl from './locales/pl/translation.json';
 import ptBr from './locales/ptBr/translation.json';
 import it from './locales/it/translation.json';
 import sr from './locales/sr/translation.json';
+import sv from './locales/sv/translation.json';
 import lt from './locales/lt/translation.json';
+import vi from './locales/vi/translation.json';
+import nbNo from './locales/nbNo/translation.json';
 
-export default () => {
-  // Load translations for each language
+/**
+ * Load translations for each language
+ */
+function createI18nInstance() {
   const resources = {
     ar,
     de,
@@ -22,10 +29,15 @@ export default () => {
     fr,
     it,
     ja,
+    kr,
     lt,
+    'nb-NO': nbNo,
     nl,
+    pl,
     'pt-BR': ptBr,
     sr,
+    sv,
+    vi,
     'zh-CN': zhCn,
     'zh-TW': zhTw,
   };
@@ -41,4 +53,6 @@ export default () => {
   });
 
   return instance;
-};
+}
+
+export default createI18nInstance;
diff --git a/src/index.js b/src/index.js
index a5ebe2e6e1c1356f03eb736c3975d6e9bef0d431..84091415de8d84c0280ce9dae386a265813f3ef8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,7 @@
 import init from './init';
 import state from './state';
 
-const exports = {
+export default {
   ...init,
   ...state,
 };
-
-export default exports;
diff --git a/src/init.js b/src/init.js
index 87aa17dcc636e841f4744ae18f76f723a49e9d19..1604375329f19d61786424596b798c19880bbe39 100644
--- a/src/init.js
+++ b/src/init.js
@@ -15,8 +15,6 @@ function viewer(config, pluginsOrStruct) {
   return new MiradorViewer(config, struct);
 }
 
-const exports = {
+export default {
   viewer,
 };
-
-export default exports;
diff --git a/src/lib/CanvasAnnotationDisplay.js b/src/lib/CanvasAnnotationDisplay.js
index 46be9292df09febf2c845e966fa86c3bcbcf65d1..437f9cfe58baab8fbc7cc57274291fc08c4c9eb6 100644
--- a/src/lib/CanvasAnnotationDisplay.js
+++ b/src/lib/CanvasAnnotationDisplay.js
@@ -22,7 +22,7 @@ export default class CanvasAnnotationDisplay {
     this.context = context;
     if (this.resource.svgSelector) {
       this.svgContext();
-    } else {
+    } else if (this.resource.fragmentSelector) {
       this.fragmentContext();
     }
   }
diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js
index 7cbd0b5c8d1838baf7ee0efdea10bb735e46f283..c9238b84d9057a70b8c96dd543ca2584177333ed 100644
--- a/src/lib/MiradorViewer.js
+++ b/src/lib/MiradorViewer.js
@@ -1,16 +1,11 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
-import deepmerge from 'deepmerge';
 import HotApp from '../components/App';
-import createStore from '../state/createStore';
-import { importConfig } from '../state/actions/config';
 import {
   filterValidPlugins,
-  getConfigFromPlugins,
-  getReducersFromPlugins,
-  getSagasFromPlugins,
 } from '../extend/pluginPreprocessing';
+import createPluggableStore from '../state/createPluggableStore';
 
 /**
  * Default Mirador instantiation
@@ -19,28 +14,25 @@ class MiradorViewer {
   /**
    */
   constructor(config, viewerConfig = {}) {
-    this.config = config;
     this.plugins = filterValidPlugins(viewerConfig.plugins || []);
+    this.config = config;
     this.store = viewerConfig.store
-      || createStore(getReducersFromPlugins(this.plugins), getSagasFromPlugins(this.plugins));
-    this.processConfig();
+      || createPluggableStore(this.config, this.plugins);
 
-    ReactDOM.render(
-      <Provider store={this.store}>
-        <HotApp plugins={this.plugins} />
-      </Provider>,
+    config.id && ReactDOM.render(
+      this.render(),
       document.getElementById(config.id),
     );
   }
 
   /**
-   * Process config with plugin configs into actions
+   * Render the mirador viewer
    */
-  processConfig() {
-    this.store.dispatch(
-      importConfig(
-        deepmerge(getConfigFromPlugins(this.plugins), this.config),
-      ),
+  render(props = {}) {
+    return (
+      <Provider store={this.store}>
+        <HotApp plugins={this.plugins} {...props} />
+      </Provider>
     );
   }
 
@@ -48,7 +40,7 @@ class MiradorViewer {
    * Cleanup method to unmount Mirador from the dom
    */
   unmount() {
-    ReactDOM.unmountComponentAtNode(document.getElementById(this.config.id));
+    this.config.id && ReactDOM.unmountComponentAtNode(document.getElementById(this.config.id));
   }
 }
 
diff --git a/src/lib/OpenSeadragonCanvasOverlay.js b/src/lib/OpenSeadragonCanvasOverlay.js
index d10bd5014b0da1dc612f778caa049449cda5b8da..fd69cf31c98b31e7469d33b8d77777f8873e9f64 100644
--- a/src/lib/OpenSeadragonCanvasOverlay.js
+++ b/src/lib/OpenSeadragonCanvasOverlay.js
@@ -58,7 +58,7 @@ export default class OpenSeadragonCanvasOverlay {
     }
 
     this.viewportOrigin = new OpenSeadragon.Point(0, 0);
-    const boundsRect = this.viewer.viewport.getBoundsNoRotate(true);
+    const boundsRect = this.viewer.viewport.getBoundsNoRotateWithMargins(true);
     this.viewportOrigin.x = boundsRect.x;
     this.viewportOrigin.y = boundsRect.y * this.imgAspectRatio;
 
diff --git a/src/lib/ThumbnailFactory.js b/src/lib/ThumbnailFactory.js
index 7cbacef410ddd9097fc44eac8abc3369156fc210..79c063f8852c82b9044640ed28e33d25b8e6de3d 100644
--- a/src/lib/ThumbnailFactory.js
+++ b/src/lib/ThumbnailFactory.js
@@ -1,12 +1,7 @@
 import { Utils } from 'manifesto.js';
 import MiradorManifest from './MiradorManifest';
 import MiradorCanvas from './MiradorCanvas';
-
-/** */
-function asArray(value) {
-  if (value === undefined) return [];
-  return Array.isArray(value) ? value : [value];
-}
+import asArray from './asArray';
 
 /** */
 function isLevel0ImageProfile(service) {
@@ -67,8 +62,66 @@ class ThumbnailFactory {
   }
 
   /**
-   * Creates a canonical image request for a thumb
-   * @param {Number} height
+   * Selects the image resource that is representative of the given canvas.
+   * @param {Object} canvas A Manifesto Canvas
+   * @return {Object} A Manifesto Image Resource
+   */
+  static getPreferredImage(canvas) {
+    const miradorCanvas = new MiradorCanvas(canvas);
+    return miradorCanvas.iiifImageResources[0] || miradorCanvas.imageResource;
+  }
+
+  /**
+   * Chooses the best available image size based on a target area (w x h) value.
+   * @param {Object} service A IIIF Image API service that has a `sizes` array
+   * @param {Number} targetArea The target area value to compare potential sizes against
+   * @return {Object|undefined} The best size, or undefined if none are acceptable
+   */
+  static selectBestImageSize(service, targetArea) {
+    const sizes = asArray(service.getProperty('sizes'));
+
+    let closestSize = {
+      default: true,
+      height: service.getProperty('height') || Number.MAX_SAFE_INTEGER,
+      width: service.getProperty('width') || Number.MAX_SAFE_INTEGER,
+    };
+
+    /** Compare the total image area to our target */
+    const imageFitness = (test) => test.width * test.height - targetArea;
+
+    /** Look for the size that's just bigger than we prefer... */
+    closestSize = sizes.reduce(
+      (best, test) => {
+        const score = imageFitness(test);
+
+        if (score < 0) return best;
+
+        return Math.abs(score) < Math.abs(imageFitness(best))
+          ? test
+          : best;
+      }, closestSize,
+    );
+
+    /** .... but not "too" big; we'd rather scale up an image than download too much */
+    if (closestSize.width * closestSize.height > targetArea * 6) {
+      closestSize = sizes.reduce(
+        (best, test) => (
+          Math.abs(imageFitness(test)) < Math.abs(imageFitness(best))
+            ? test
+            : best
+        ), closestSize,
+      );
+    }
+
+    if (closestSize.default) return undefined;
+
+    return closestSize;
+  }
+
+  /**
+   * Determines the appropriate thumbnail to use to represent an Image Resource.
+   * @param {Object} resource The Image Resource from which to derive a thumbnail
+   * @return {Object} The thumbnail URL and any spatial dimensions that can be determined
    */
   iiifThumbnailUrl(resource) {
     let size;
@@ -84,63 +137,26 @@ class ThumbnailFactory {
 
     const service = iiifImageService(resource);
 
-    if (!service) return undefined;
+    if (!service) return ThumbnailFactory.staticImageUrl(resource);
 
     const aspectRatio = resource.getWidth()
       && resource.getHeight()
       && (resource.getWidth() / resource.getHeight());
+    const target = (requestedMaxWidth && requestedMaxHeight)
+      ? requestedMaxWidth * requestedMaxHeight
+      : maxHeight * maxWidth;
+    const closestSize = ThumbnailFactory.selectBestImageSize(service, target);
 
-    // just bail to a static image, even though sizes might provide something better
-    if (isLevel0ImageProfile(service)) {
-      const sizes = asArray(service.getProperty('sizes'));
-      const serviceHeight = service.getProperty('height');
-      const serviceWidth = service.getProperty('width');
-
-      const target = (requestedMaxWidth && requestedMaxHeight)
-        ? requestedMaxWidth * requestedMaxHeight
-        : maxHeight * maxWidth;
-
-      let closestSize = {
-        default: true,
-        height: serviceHeight || Number.MAX_SAFE_INTEGER,
-        width: serviceWidth || Number.MAX_SAFE_INTEGER,
-      };
-
-      /** Compare the total image area to our target */
-      const imageFitness = (test) => test.width * test.height - target;
-
-      /** Look for the size that's just bigger than we prefer... */
-      closestSize = sizes.reduce(
-        (best, test) => {
-          const score = imageFitness(test);
-
-          if (score < 0) return best;
-
-          return Math.abs(score) < Math.abs(imageFitness(best))
-            ? test
-            : best;
-        }, closestSize,
-      );
-
-      /** .... but not "too" big; we'd rather scale up an image than download too much */
-      if (closestSize.width * closestSize.height > target * 6) {
-        closestSize = sizes.reduce(
-          (best, test) => (
-            Math.abs(imageFitness(test)) < Math.abs(imageFitness(best))
-              ? test
-              : best
-          ), closestSize,
-        );
-      }
-
-      /** Bail if the best available size is the full size.. maybe we'll get lucky with the @id */
-      if (closestSize.default && !serviceHeight && !serviceWidth) {
-        return ThumbnailFactory.staticImageUrl(resource);
-      }
-
+    if (closestSize) {
+      // Embedded service advertises an appropriate size
       width = closestSize.width;
       height = closestSize.height;
       size = `${width},${height}`;
+    } else if (isLevel0ImageProfile(service)) {
+      /** Bail if the best available size is the full size.. maybe we'll get lucky with the @id */
+      if (!service.getProperty('height') && !service.getProperty('width')) {
+        return ThumbnailFactory.staticImageUrl(resource);
+      }
     } else if (requestedMaxHeight && requestedMaxWidth) {
       // IIIF level 2, no problem.
       if (isLevel2ImageProfile(service)) {
@@ -176,7 +192,7 @@ class ThumbnailFactory {
     const region = 'full';
     const quality = Utils.getImageQuality(service.getProfile());
     const id = service.id.replace(/\/+$/, '');
-    const format = 'jpg';
+    const format = this.getFormat(service);
     return {
       height,
       url: [id, region, size, 0, `${quality}.${format}`].join('/'),
@@ -184,77 +200,111 @@ class ThumbnailFactory {
     };
   }
 
-  /** */
-  getThumbnail(resource, { requireIiif, quirksMode }) {
-    if (!resource) return undefined;
-    const thumb = resource.getThumbnail();
-    if (thumb && iiifImageService(thumb)) return this.iiifThumbnailUrl(thumb);
+  /**
+   * Figure out what format thumbnail to use by looking at the preferred formats
+   * on offer, and selecting a format shared in common with the application's
+   * preferred format list.
+   *
+   * Fall back to jpg, which is required to work for all IIIF services.
+   */
+  getFormat(service) {
+    const { preferredFormats = [] } = this.iiifOpts;
+    const servicePreferredFormats = service.getProperty('preferredFormats');
 
-    if (requireIiif) return undefined;
-    if (thumb && typeof thumb.__jsonld !== 'string') return ThumbnailFactory.staticImageUrl(thumb);
+    if (!servicePreferredFormats) return 'jpg';
 
-    if (!quirksMode) return undefined;
+    const filteredFormats = servicePreferredFormats.filter(
+      value => preferredFormats.includes(value),
+    );
 
-    return (thumb && typeof thumb.__jsonld === 'string') ? { url: thumb.__jsonld } : undefined;
-  }
+    // this is a format found in common between the preferred formats of the service
+    // and the application
+    if (filteredFormats[0]) return filteredFormats[0];
 
-  /** */
-  getResourceThumbnail(resource) {
-    const thumb = this.getThumbnail(resource, { requireIiif: true });
-
-    if (thumb) return thumb;
+    // IIIF Image API guarantees jpg support; if it wasn't provided by the service
+    // but the application is fine with it, we might as well try it.
+    if (!servicePreferredFormats.includes('jpg') && preferredFormats.includes('jpg')) {
+      return 'jpg';
+    }
 
-    if (iiifImageService(resource)) return this.iiifThumbnailUrl(resource);
-    if (['image', 'dctypes:Image'].includes(resource.getProperty('type'))) return ThumbnailFactory.staticImageUrl(resource);
+    // there were no formats in common, and the application didn't want jpg... so
+    // just trust that the IIIF service is advertising something useful?
+    if (servicePreferredFormats[0]) return servicePreferredFormats[0];
 
-    return this.getThumbnail(resource, { quirksMode: true, requireIiif: false });
+    // JPG support is guaranteed by the spec, so it's a good worst-case fallback
+    return 'jpg';
   }
 
-  /** */
-  getIIIFThumbnail(canvas) {
-    const thumb = this.getThumbnail(canvas, { requireIiif: true });
-    if (thumb) return thumb;
+  /**
+   * Determines the content resource from which to derive a thumbnail to represent a given resource.
+   * This method is recursive.
+   * @param {Object} resource A IIIF resource to derive a thumbnail from
+   * @return {Object|undefined} The Image Resource to derive a thumbnail from, or undefined
+   * if no appropriate resource exists
+   */
+  getSourceContentResource(resource) {
+    const thumbnail = resource.getThumbnail();
+
+    // Any resource type may have a thumbnail
+    if (thumbnail) {
+      if (typeof thumbnail.__jsonld === 'string') return thumbnail.__jsonld;
+
+      // Prefer an image's ImageService over its image's thumbnail
+      // Note that Collection, Manifest, and Canvas don't have `getType()`
+      if (!resource.isCollection() && !resource.isManifest() && !resource.isCanvas()) {
+        if (resource.getType() === 'image' && iiifImageService(resource) && !iiifImageService(thumbnail)) {
+          return resource;
+        }
+      }
 
-    const miradorCanvas = new MiradorCanvas(canvas);
+      return thumbnail;
+    }
 
-    const preferredCanvasResource = miradorCanvas.iiifImageResources[0]
-     || canvas.imageResource;
+    if (resource.isCollection()) {
+      const firstManifest = resource.getManifests()[0];
+      if (firstManifest) return this.getSourceContentResource(firstManifest);
 
-    return (preferredCanvasResource && this.getResourceThumbnail(preferredCanvasResource))
-      || this.getThumbnail(canvas, { quirksMode: true, requireIiif: false });
-  }
+      return undefined;
+    }
 
-  /** */
-  getManifestThumbnail(manifest) {
-    const thumb = this.getThumbnail(manifest, { requireIiif: true });
-    if (thumb) return thumb;
+    if (resource.isManifest()) {
+      const miradorManifest = new MiradorManifest(resource);
+      const canvas = miradorManifest.startCanvas || miradorManifest.canvasAt(0);
+      if (canvas) return this.getSourceContentResource(canvas);
 
-    const miradorManifest = new MiradorManifest(manifest);
-    const canvas = miradorManifest.startCanvas || miradorManifest.canvasAt(0);
+      return undefined;
+    }
 
-    return (canvas && this.getIIIFThumbnail(canvas))
-      || this.getThumbnail(manifest, { quirksMode: true, requireIiif: false });
-  }
+    if (resource.isCanvas()) {
+      const image = ThumbnailFactory.getPreferredImage(resource);
+      if (image) return this.getSourceContentResource(image);
 
-  /** */
-  getCollectionThumbnail(collection) {
-    const thumb = this.getThumbnail(collection, { requireIiif: true });
-    if (thumb) return thumb;
+      return undefined;
+    }
 
-    const firstManifest = this.resource.getManifests()[0];
+    if (resource.getType() === 'image') {
+      return resource;
+    }
 
-    return (firstManifest && this.getManifestThumbnail(firstManifest))
-      || this.getThumbnail(collection, { quirksMode: true, requireIiif: false });
+    return undefined;
   }
 
-  /** */
+  /**
+   * Gets a thumbnail representing the resource.
+   * @return {Object|undefined} A thumbnail representing the resource, or undefined if none could
+   * be determined
+   */
   get() {
     if (!this.resource) return undefined;
 
-    if (this.resource.isCanvas()) return this.getIIIFThumbnail(this.resource);
-    if (this.resource.isManifest()) return this.getManifestThumbnail(this.resource);
-    if (this.resource.isCollection()) return this.getCollectionThumbnail(this.resource);
-    return this.getResourceThumbnail(this.resource, { requireIiif: true });
+    // Determine which content resource we should use to derive a thumbnail
+    const sourceContentResource = this.getSourceContentResource(this.resource);
+    if (!sourceContentResource) return undefined;
+
+    // Special treatment for external resources
+    if (typeof sourceContentResource === 'string') return { url: sourceContentResource };
+
+    return this.iiifThumbnailUrl(sourceContentResource);
   }
 }
 
@@ -263,4 +313,4 @@ function getBestThumbnail(resource, iiifOpts) {
   return new ThumbnailFactory(resource, iiifOpts).get();
 }
 
-export default getBestThumbnail;
+export { getBestThumbnail as default, ThumbnailFactory };
diff --git a/src/lib/asArray.js b/src/lib/asArray.js
new file mode 100644
index 0000000000000000000000000000000000000000..31557e924d1af3aa5fa01992f46470259f79bfac
--- /dev/null
+++ b/src/lib/asArray.js
@@ -0,0 +1,11 @@
+/**
+ */
+export default function asArray(value) {
+  if (value === undefined) return [];
+
+  if (!Array.isArray(value)) {
+    return [value];
+  }
+
+  return value;
+}
diff --git a/src/lib/htmlRules.js b/src/lib/htmlRules.js
index 43fd823275b1bf50c98b29f68b4f8ef34353c09a..f50c3ae246f8b1219a2fe40af0a7947e8c798be9 100644
--- a/src/lib/htmlRules.js
+++ b/src/lib/htmlRules.js
@@ -18,9 +18,11 @@ const mirador2 = {
   ALLOWED_TAGS: ['a', 'b', 'br', 'i', 'img', 'p', 'span', 'strong', 'em', 'ul', 'ol', 'li'],
 };
 
-export default {
+const htmlRules = {
   iiif,
   liberal,
   mirador2,
   noHtml,
 };
+
+export default htmlRules;
diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json
index c62314460a6d2dda76c8eb2ff16aba874248365b..b1d3ca8b74c2123e5fadf13309ce9e5a181057a0 100644
--- a/src/locales/de/translation.json
+++ b/src/locales/de/translation.json
@@ -25,7 +25,7 @@
     "closeAddResourceMenu": "Ressourcenliste schließen",
     "closeCompanionWindow": "Hilfsfenster schließen",
     "closeWindow": "Fenster schließen",
-    "collapseSection": "Bereich {{section}} zuklappen",
+    "collapseSection": "Bereich \"{{section}}\" zuklappen",
     "collapseSidePanel": "Seitenleiste zuklappen",
     "itemList": "Kompaktliste",
     "continue": "Fortfahren",
@@ -48,7 +48,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Es ist ein Fehler aufgetreten",
     "exitFullScreen": "Vollbildmodus verlassen",
-    "expandSection": "Bereich {{section}} aufklappen",
+    "expandSection": "Bereich \"{{section}}\" aufklappen",
     "expandSidePanel": "Seitenleiste aufklappen",
     "exportCopied": "Die Konfiguration der Arbeitsfläche wurde in die Zwischenablage kopiert.",
     "fetchManifest": "Hinzufügen",
@@ -114,6 +114,7 @@
     "searchNextResult": "Nächster Treffer",
     "searchNoResults": "Keine Treffer",
     "searchPreviousResult": "Vorheriger Treffer",
+    "searchResultsRemaining": "{{numLeft}} weitere",
     "searchSubmitAria": "Suchen",
     "searchTitle": "Suche",
     "selectWorkspaceMenu": "Wählen Sie einen Arbeitsflächentyp",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 8c81ce23800a31175cadc714199b2f1805cc540e..1fe15e7418ee188bd100aacc7c2ef2ac7c12bf76 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -26,7 +26,7 @@
     "closeAddResourceMenu": "Close resource list",
     "closeCompanionWindow": "Close panel",
     "closeWindow": "Close window",
-    "collapseSection": "Collapse {{section}} section",
+    "collapseSection": "Collapse \"{{section}}\" section",
     "collapseSidePanel": "Collapse sidebar",
     "collection": "Collection",
     "itemList": "Item list",
@@ -50,7 +50,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "An error occurred",
     "exitFullScreen": "Exit full screen",
-    "expandSection": "Expand {{section}} section",
+    "expandSection": "Expand \"{{section}}\" section",
     "expandSidePanel": "Expand sidebar",
     "exportCopied": "The workspace configuration was copied to your clipboard",
     "fetchManifest": "Add",
@@ -91,9 +91,11 @@
     "mosaicDescription": "Move and size windows in relation to each other, within the visible frame.",
     "moveCompanionWindowToBottom": "Move to bottom",
     "moveCompanionWindowToRight": "Move to right",
+    "multipartCollection": "Multipart Collection",
     "nextCanvas": "Next item",
     "noItemSelected": "No item selected",
-    "numItems": "{{number}} items",
+    "numItems": "{{number}} item",
+    "numItems_plural": "{{number}} items",
     "off": "Off",
     "openCompanionWindow_annotations": "Annotations",
     "openCompanionWindow_attribution": "Rights",
@@ -117,10 +119,12 @@
     "searchNextResult": "Next result",
     "searchNoResults": "No results found",
     "searchPreviousResult": "Previous result",
+    "searchResultsRemaining": "{{numLeft}} remaining",
     "searchSubmitAria": "Submit search",
     "searchTitle": "Search",
     "selectWorkspaceMenu": "Select workspace type",
-    "showingNumAnnotations": "Showing {{number}} annotations",
+    "showingNumAnnotations": "Showing {{number}} annotation",
+    "showingNumAnnotations_plural": "Showing {{number}} annotations",
     "showCollection": "Show collection",
     "showZoomControls": "Show zoom controls",
     "sidebarPanelsNavigation": "Sidebar panels navigation",
@@ -133,11 +137,14 @@
     "thumbnailNavigation": "Thumbnails",
     "thumbnails": "Thumbnails",
     "toggleWindowSideBar": "Toggle sidebar",
-    "totalCollections": "{{count}} collections",
-    "totalManifests": "{{count}} manifests",
+    "totalCollections": "{{count}} collection",
+    "totalCollections_plural": "{{count}} collections",
+    "totalManifests": "{{count}} manifest",
+    "totalManifests_plural": "{{count}} manifests",
     "tryAgain": "Try again",
     "untitled": "[Untitled]",
     "view": "View",
+    "viewWorkspaceConfiguration": "View workspace configuration",
     "welcome": "Welcome to Mirador",
     "window": "Window: {{label}}",
     "windowMenu": "Window views & thumbnail display",
diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json
index 6c97b9e3861bcd05bb79fc4a195fdf23b6213fe9..4bb09cbbe478aa6cf25a46382703ca6a288d3711 100644
--- a/src/locales/fr/translation.json
+++ b/src/locales/fr/translation.json
@@ -25,7 +25,7 @@
     "closeAddResourceMenu": "Fermer la liste des ressources",
     "closeCompanionWindow": "Fermer le panneau",
     "closeWindow": "Fermer cette fenêtre",
-    "collapseSection": "Replier la section {{section}} ",
+    "collapseSection": "Replier la section \"{{section}}\"",
     "collapseSidePanel": "Replier le panneau",
     "itemList": "Liste compacte",
     "continue": "Continuer",
@@ -48,7 +48,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Une erreur est survenue",
     "exitFullScreen": "Quitter le plein écran",
-    "expandSection": "Déplier la section {{section}}",
+    "expandSection": "Déplier la section \"{{section}}\"",
     "expandSidePanel": "Déplier le panneau",
     "exportCopied": "La configuration de l'espace de travail a été copiée dans votre presse-papier",
     "fetchManifest": "Ajouter",
@@ -91,7 +91,8 @@
     "moveCompanionWindowToRight": "Déplacer à droite",
     "nextCanvas": "Suivant",
     "noItemSelected": "Aucun élément sélectionné",
-    "numItems": "{{number}} images",
+    "numItems": "{{number}} image",
+    "numItems_plural": "{{number}} images",
     "off": "aucun",
     "openCompanionWindow_annotations": "Annotations",
     "openCompanionWindow_attribution": "Droits",
@@ -118,7 +119,8 @@
     "searchSubmitAria": "Lancer la recherche",
     "searchTitle": "Rechercher",
     "selectWorkspaceMenu": "Changer de type d'espace de travail",
-    "showingNumAnnotations": "{{number}} annotations affichées",
+    "showingNumAnnotations": "{{number}} annotation affichée",
+    "showingNumAnnotations_plural": "{{number}} annotations affichées",
     "showCollection": "Voir la collection",
     "showZoomControls": "Activer les commandes de zoom",
     "sidebarPanelsNavigation": "Navigation dans les panneaux latéraux",
@@ -131,8 +133,10 @@
     "thumbnailNavigation": "Vignettes",
     "thumbnails": "Afficher les vignettes",
     "toggleWindowSideBar": "Afficher le menu latéral",
-    "totalCollections": "{{count}} collections",
-    "totalManifests": "{{count}} manifestes",
+    "totalCollections": "{{count}} collection",
+    "totalCollections_plural": "{{count}} collections",
+    "totalManifests": "{{count}} manifeste",
+    "totalManifests_plural": "{{count}} manifestes",
     "tryAgain": "Essayer à nouveau",
     "untitled": "[Sans titre]",
     "view": "Voir les images en mode",
@@ -141,7 +145,7 @@
     "windowMenu": "Options de fenêtre",
     "windowNavigation": "Navigation dans les fenêtres",
     "windowPluginButtons": "Options",
-    "windowPluginMenu": "Options de fenêtre",
+    "windowPluginMenu": "Autres options et outils",
     "workspace": "Espace de travail",
     "workspaceNavigation": "Menu de l'espace de travail",
     "workspaceFullScreen": "Plein écran",
diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json
index c3b4ac8c793cb6649c478ae7b6aeef7a0252a073..9dfae93ef25a7a294182395a950402cd3cfcb2de 100644
--- a/src/locales/it/translation.json
+++ b/src/locales/it/translation.json
@@ -1,10 +1,11 @@
 {
   "translation": {
+    "aboutMirador": "Informazioni su Mirador",
     "aboutThisItem": "Informazioni sull'oggetto",
-    "addedFromUrl": "(Aggiunto dall'URL)",
     "addManifestUrl": "URL della risorsa",
     "addManifestUrlHelp": "L'URL di una risorsa IIIF",
     "addResource": "Aggiungi una risorsa",
+    "addedFromUrl": "(Aggiunto dall'URL)",
     "annotationCanvasLabel_1/1": "Oggetto: [{{label}}]",
     "annotationCanvasLabel_1/2": "Sinistra: [{{label}}]",
     "annotationCanvasLabel_2/2": "Destra: [{{label}}]",
@@ -20,13 +21,14 @@
     "canvasIndex": "Indice",
     "changeTheme": "Cambia tema",
     "clearSearch": "pulisci",
+    "close": "Chiudi",
     "closeAddResourceForm": "Chiudi il modulo",
     "closeAddResourceMenu": "Chiudi la lista di risorse",
     "closeCompanionWindow": "Chiudi il pannello",
     "closeWindow": "Chiudi finestra",
-    "collapseSection": "Collassa la sezione {{section}}",
+    "collapseSection": "Collassa la sezione \"{{section}}\"",
     "collapseSidePanel": "Collassa la barra laterale",
-    "itemList": "Lista compatta",
+    "collection": "Collezione",
     "continue": "Continua",
     "copy": "Copia",
     "currentItem": "Oggetto corrente",
@@ -34,35 +36,49 @@
     "currentItem_1/2": "Sinistra",
     "currentItem_2/2": "Destra",
     "dark": "Tema scuro",
+    "digitizedView": "Digitized view",
     "dismiss": "Dismiss",
-    "highlightAllAnnotations": "Evidenzia tutto",
+    "displayNoAnnotations": "Nascondi evidenziazione",
     "downloadExport": "Esporta il workspace",
     "downloadExportWorkspace": "Esporta il workspace",
     "elastic": "Elastico",
     "elasticDescription": "Muovi e ridimensiona le finestre liberamente in un workspace illimitato. Le finestre possono sovrapporsi.",
     "emptyResourceList": "La tua lista di risorse è vuota.",
+    "error": "Errore",
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Si è verificato un errore.",
     "exitFullScreen": "Esci da schermo intero",
-    "expandSection": "Espandi la sezione {{section}}",
+    "expandSection": "Espandi la sezione \"{{section}}\"",
     "expandSidePanel": "Espandi la barra laterale",
+    "exportCopied": "La configurazione del workspace è stata copiata nella tua clipboard",
     "fetchManifest": "Aggiungi",
     "fullScreen": "Schermo intero",
     "gallery": "Galleria",
     "hideZoomControls": "Nascondi i controlli di zoom",
+    "highlightAllAnnotations": "Evidenzia tutto",
     "iiif_homepage": "Informazioni su questa risorsa",
     "iiif_manifest": "IIIF manifest",
     "iiif_renderings": "Formati alternativi",
     "iiif_seeAlso": "Vedi anche",
-    "import" : "Importa",
+    "import": "Importa",
     "importWorkspace": "Importa workspace",
     "importWorkspaceHint": "Incolla una configurazione di Mirador 3 da importare",
     "item": "Oggetto: {{label}}",
+    "itemList": "Lista compatta",
+    "jsError": "Dettagli tecnici",
+    "jsStack": "{{ stack }}",
     "language": "Lingua",
+    "layer_hide": "Nascondi livello",
+    "layer_move": "Sposta livello",
+    "layer_moveToTop": "Sposta il livello in alto",
+    "layer_opacity": "Opacità del livello",
+    "layer_show": "Visualizza livello",
+    "layers": "Livelli",
     "light": "Tema chiaro",
     "links": "Link",
     "listAllOpenWindows": "Vai alla finestra",
     "login": "Entra",
+    "logout": "Esci",
     "manifestError": "La risorsa non può essere aggiunta:",
     "maximizeWindow": "Massimizza la finestra",
     "minimizeWindow": "Minimizza la finestra",
@@ -76,12 +92,15 @@
     "moveCompanionWindowToBottom": "Sposta in fondo",
     "moveCompanionWindowToRight": "Sposta a destra",
     "nextCanvas": "Prossimo oggetto",
+    "noItemSelected": "Nessun oggetto selezionato",
     "numItems": "{{number}} oggetti",
+    "numItems_plural": "{{number}} oggetti",
     "off": "Off",
     "openCompanionWindow_annotations": "Annotazioni",
     "openCompanionWindow_attribution": "Diritti",
     "openCompanionWindow_canvas": "Indice",
     "openCompanionWindow_info": "Informazioni",
+    "openCompanionWindow_layers": "Livelli",
     "openCompanionWindow_search": "Cerca",
     "openInCompanionWindow": "Apri in un pannello separato",
     "openWindows": "Finestre aperte in questo momento",
@@ -94,27 +113,37 @@
     "retry": "Riprova",
     "right": "Right",
     "rights": "Licenza",
+    "scroll": "Scorri",
     "searchInputLabel": "termini di ricerca",
     "searchNextResult": "Prossimo risultato",
     "searchNoResults": "Nessun risultato",
     "searchPreviousResult": "Risultato precedente",
+    "searchResultsRemaining": "{{numLeft}} rimanenti",
     "searchSubmitAria": "Cerca",
     "searchTitle": "Cerca",
     "selectWorkspaceMenu": "Selezione il tipo di workspace",
-    "showingNumAnnotations": "Sto mostrando {{number}} annotazioni",
+    "showCollection": "Visualizza la collezione",
     "showZoomControls": "Mostra i controlli di zoom",
+    "showingNumAnnotations": "Sto mostrando {{number}} annotazioni",
+    "showingNumAnnotations_plural": "Visualizzando {{number}} annotazioni",
     "sidebarPanelsNavigation": "Navigazione dei pannelli della barra laterale",
     "single": "Singolo",
     "startHere": "Inizia qui",
     "suggestSearch": "Cerca in questo documento: \"{{ query }}\"",
+    "tableOfContentsList": "Sommario",
     "theme": "Tema",
     "thumbnailList": "Lista thumbnail",
     "thumbnailNavigation": "Thumbnails",
     "thumbnails": "Thumbnails",
     "toggleWindowSideBar": "Apri/Chiudi la barra",
+    "totalCollections": "{{count}} collezione",
+    "totalCollections_plural": "{{count}} collezioni",
+    "totalManifests": "{{count}} manifest",
+    "totalManifests_plural": "{{count}} manifests",
     "tryAgain": "Riprova",
     "untitled": "[senza titolo]",
     "view": "Visualizza",
+    "viewWorkspaceConfiguration": "Visualizza la configurazione del workspace",
     "welcome": "Benvenuto in Mirador",
     "window": "Finestra: {{label}}",
     "windowMenu": "Visualizzazioni finestra e display thumbnail",
@@ -124,6 +153,7 @@
     "workspace": "Workspace",
     "workspaceFullScreen": "Schermo intero",
     "workspaceMenu": "Configurazioni Workspace",
+    "workspaceNavigation": "Navigazione del workspace",
     "workspaceOptions": "Opzioni Workspace",
     "workspaceSelectionTitle": "Seleziona il tipo di workspace",
     "zoomIn": "Zoom in",
diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json
index 4f23e62ea8b1366619b82255602d10173ebc6d07..866d13b35107d1f1a85c68877696bbf636f83358 100644
--- a/src/locales/ja/translation.json
+++ b/src/locales/ja/translation.json
@@ -1,5 +1,6 @@
 {
   "translation": {
+    "aboutMirador": "Project Miradorについて",
     "aboutThisItem": "この資料について",
     "addedFromUrl": "(URLで追加)",
     "addManifestUrl": "資料のURL",
@@ -11,70 +12,95 @@
     "annotations": "アノテーション",
     "attribution": "帰属",
     "attributionTitle": "権利",
+    "authenticationFailed": "認証失敗",
+    "authenticationRequired": "フルアクセスには認証が必要",
+    "backToResults": "結果に戻る",
     "book": "見開き",
     "bottom": "下部",
     "cancel": "キャンセル",
     "canvasIndex": "インデックス",
     "changeTheme": "テーマの変更",
+    "clearSearch": "クリア",
+    "close": "閉じる",
     "closeAddResourceForm": "フォームを閉じる",
     "closeAddResourceMenu": "資料一覧を閉じる",
     "closeCompanionWindow": "パネルを閉じる",
     "closeWindow": "ウインドウを閉じる",
-    "collapseSection": " {{section}} セクションを畳む",
+    "collapseSection": "{{section}} セクションを畳む",
     "collapseSidePanel": "サイドバーを畳む",
-    "itemList": "一覧を最小化",
+    "collection": "コレクション",
+    "itemList": "アイテム一覧",
+    "continue": "続ける",
     "copy": "コピー",
     "currentItem": "現在のアイテム",
     "currentItem_1/1": "現在のアイテム",
     "currentItem_1/2": "左",
     "currentItem_2/2": "右",
     "dark": "ダークなテーマ",
+    "digitizedView": "デジタルビュー",
     "dismiss": "片付け",
     "highlightAllAnnotations": "すべてを表示",
+    "displayNoAnnotations": "アノテーションを非表示",
     "downloadExport": "ワークスペースをエクスポート",
     "downloadExportWorkspace": "ワークスペースをエクスポート",
     "elastic": "伸縮",
     "elasticDescription": "自由なウインドウの伸縮",
     "emptyResourceList": "資料の一覧が空です",
+    "error": "エラー",
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "エラー発生",
     "exitFullScreen": "全画面を解除",
     "expandSection": "セクション {{section}}を拡大",
     "expandSidePanel": "サイドバーを拡大",
+    "exportCopied": "ワークスペースの設定がクリップボードにコピーされました。",
     "fetchManifest": "追加",
     "fullScreen": "全画面",
     "gallery": "ギャラリー",
     "hideZoomControls": "zoom制御を隠す",
-    "iiif_homepage": "この資料について",
+    "iiif_homepage": "IIIFホームページ",
     "iiif_manifest": "IIIF マニフェスト",
     "iiif_renderings": "別の形式",
     "iiif_seeAlso": "参照",
-    "import" : "取り込み",
+    "import": "取り込み",
     "importWorkspace": "ワークスペースの取り込み",
     "importWorkspaceHint": "Mirador3の設定を貼り付け",
-    "アイテム": "アイテム: {{label}}",
+    "item": "アイテム: {{label}}",
+    "jsError": "技術的な詳細",
+    "jsStack": "{{ stack }}",
     "language": "言語",
+    "layer_hide": "レイヤーを隠す",
+    "layer_move": "レイヤーを動かす",
+    "layer_opacity": "レイヤーの透過度",
+    "layer_show": "レイヤーを表示",
+    "layer_moveToTop": "レイヤーをトップへ",
+    "layers": "レイヤー",
     "light": "明るいテーマ",
     "links": "リンク",
     "listAllOpenWindows": "ウインドウにジャンプ",
     "login": "ログイン",
+    "logout": "ログアウト",
     "manifestError": "資料追加に失敗:",
     "maximizeWindow": "ウインドウを最大化",
     "minimizeWindow": "ウインドウを最小化",
     "mirador": "Mirador",
     "miradorResources": "Mirador資料",
     "miradorViewer": "Miradorビューワ",
+    "more": "さらに...",
+    "moreResults": "さらに結果を",
     "mosaic": "モザイク",
     "mosaicDescription": "モザイク表示",
     "moveCompanionWindowToBottom": "下部に移動",
     "moveCompanionWindowToRight": "右に移動",
     "nextCanvas": "次のアイテム",
+    "noItemSelected": "アイテムが未選択",
     "numItems": "{{number}} アイテム",
     "off": "オフ",
     "openCompanionWindow_annotations": "アノテーション",
     "openCompanionWindow_attribution": "権利",
     "openCompanionWindow_canvas": "インデックス",
     "openCompanionWindow_info": "情報",
+    "openCompanionWindow_layers": "レイヤー",
+    "openCompanionWindow_search": "検索",
     "openInCompanionWindow": "別のパネルで開く",
     "openWindows": "現在開いているウインドウ",
     "pagination": "{{current}} of {{total}}",
@@ -83,28 +109,43 @@
     "previousCanvas": "前のアイテム",
     "related": "関連",
     "resource": "資料",
+    "retry": "リトライ",
     "right": "右側",
     "rights": "利用条件",
+    "scroll": "スクロール",
+    "searchInputLabel": "検索語",
+    "searchNextResult": "次の結果",
+    "searchNoResults": "ヒットせず",
+    "searchPreviousResult": "前の結果",
+    "searchSubmitAria": "検索",
+    "searchTitle": "検索",
     "selectWorkspaceMenu": "ワークスペースタイプの選択",
     "showingNumAnnotations": "アノテーション {{number}} を表示",
+    "showCollection": "コレクションを表示",
     "showZoomControls": "ズーム操作を表示",
     "sidebarPanelsNavigation": "サイドバーパネルの操作",
     "single": "単一",
     "startHere": "ここから始める",
+    "suggestSearch": "この文書を \"{{ query }}\" で検索",
+    "tableOfContentsList": "目次",
     "theme": "テーマ",
     "thumbnailList": "サムネイル一覧",
     "thumbnailNavigation": "サムネイル",
     "thumbnails": "サムネイル表示",
     "toggleWindowSideBar": "サイドバー切り替え",
+    "totalCollections": "{{count}} コレクション",
+    "totalManifests": "{{count}} マニフェスト",
     "tryAgain": "もう一度試す",
     "untitled": "[タイトル無し]",
     "view": "表示の仕方",
     "welcome": "Miradorにようこそ",
     "window": "{{label}} ウインドウ",
-    "windowMenu": "ウインドウオプション",
+    "windowMenu": "ウインドウメニュー",
     "windowNavigation": "ウィンドウ操作",
     "windowPluginButtons": "オプション",
+    "windowPluginMenu": "ウインドウオプション",
     "workspace": "ワークスペース",
+    "workspaceNavigation": "ワークスペースナビ",
     "workspaceFullScreen": "全画面",
     "workspaceMenu": "ワークスペースの設定",
     "workspaceOptions": "ワークスペースのオプション",
diff --git a/src/locales/kr/translation.json b/src/locales/kr/translation.json
new file mode 100644
index 0000000000000000000000000000000000000000..ce2b9328262957ac3a25257161d0d0d169f2330e
--- /dev/null
+++ b/src/locales/kr/translation.json
@@ -0,0 +1,163 @@
+{
+  "translation": {
+    "aboutMirador": "Mirador 프로젝트에 대하여",
+    "aboutThisItem": "해당 아이템에 대하여",
+    "addedFromUrl": "(URL에서 추가됨)",
+    "addManifestUrl": "리소스의 위치",
+    "addManifestUrlHelp": "IIIF 리소스의 URL",
+    "addResource": "리소스 추가",
+    "annotationCanvasLabel_1/1": "아이템: [{{label}}]",
+    "annotationCanvasLabel_1/2": "왼쪽: [{{label}}]",
+    "annotationCanvasLabel_2/2": "오른쪽: [{{label}}]",
+    "annotations": "주석",
+    "attribution": "귀속",
+    "attributionTitle": "권리",
+    "authenticationFailed": "인증 실패.",
+    "authenticationRequired": "모든 정보에 접근하기 위해선 인증이 필요합니다.",
+    "backToResults": "결과로 돌아가기",
+    "book": "책",
+    "bottom": "아래",
+    "cancel": "취소",
+    "canvasIndex": "인덱스",
+    "changeTheme": "테마 변경",
+    "clearSearch": "지우기",
+    "close": "닫기",
+    "closeAddResourceForm": "폼 닫기",
+    "closeAddResourceMenu": "리소스 목록 닫기",
+    "closeCompanionWindow": "패널 닫기",
+    "closeWindow": "윈도우 닫기",
+    "collapseSection": "\"{{section}}\" 섹션 접기",
+    "collapseSidePanel": "사이드바 접기",
+    "collection": "컬렉션",
+    "itemList": "아이템 목록",
+    "continue": "계속하기",
+    "copy": "복사",
+    "currentItem": "현재 아이템",
+    "currentItem_1/1": "현재 아이템",
+    "currentItem_1/2": "왼쪽",
+    "currentItem_2/2": "오른쪽",
+    "dark": "다크 모드",
+    "digitizedView": "디지털 뷰",
+    "dismiss": "무시하기",
+    "highlightAllAnnotations": "모두 하이라이트하기",
+    "displayNoAnnotations": "하이라이트 해제",
+    "downloadExport": "작업공간 내보내기",
+    "downloadExportWorkspace": "작업공간 내보내기",
+    "elastic": "신축성",
+    "elasticDescription": "무한한 작업공간에서 윈도우를 자유롭게 움직이고 조절해보세요. 윈도우는 서로 겹칠 수 있습니다.",
+    "emptyResourceList": "리소스 목록이 비어있습니다",
+    "error": "오류",
+    "errorDialogConfirm": "허락",
+    "errorDialogTitle": "오류 발생",
+    "exitFullScreen": "전체화면에서 나가기",
+    "expandSection": "\"{{section}}\" 섹션 확장",
+    "expandSidePanel": "사이드바 확장",
+    "exportCopied": "작업공간 환경설정을 클립보드에 복사했습니다",
+    "fetchManifest": "추가하기",
+    "fullScreen": "전체화면",
+    "gallery": "갤러리",
+    "hideZoomControls": "확대/축소 기능 숨기기",
+    "iiif_homepage": "해당 리소스에 대하여",
+    "iiif_manifest": "IIIF 매니페스트",
+    "iiif_renderings": "대체 포맷",
+    "iiif_seeAlso": "참고",
+    "import" : "가져오기",
+    "importWorkspace": "작업공간 가져오기",
+    "importWorkspaceHint": "Mirador 3 환경설정 가져와 붙여넣기",
+    "item": "아이템: {{label}}",
+    "jsError": "기술적인 세부사항",
+    "jsStack": "{{ stack }}",
+    "language": "언어",
+    "layer_hide": "레이어 숨기기",
+    "layer_move": "레이어 움직이기",
+    "layer_opacity": "레이어 불투명도",
+    "layer_show": "레이어 보이기",
+    "layer_moveToTop": "레이어를 위로 옮기기",
+    "layers": "레이어",
+    "light": "라이트 모드",
+    "links": "링크",
+    "listAllOpenWindows": "윈도우로 이동하기",
+    "login": "로그인",
+    "logout": "로그아웃",
+    "manifestError": "리소스를 추가할 수 없습니다:",
+    "maximizeWindow": "윈도우 크기 최대화",
+    "minimizeWindow": "윈도우 크기 최소화",
+    "mirador": "Mirador",
+    "miradorResources": "Mirador 리소스",
+    "miradorViewer": "Mirador 뷰어",
+    "more": "더 보기...",
+    "moreResults": "결과 더 보기",
+    "mosaic": "모자이크",
+    "mosaicDescription": "프레임 내에서 윈도우를 이동하고 크기를 조정합니다.",
+    "moveCompanionWindowToBottom": "아래로 옮기기",
+    "moveCompanionWindowToRight": "오른쪽으로 옮기기",
+    "nextCanvas": "다음 아이템",
+    "noItemSelected": "아이템이 선택되지 않았습니다",
+    "numItems": "{{number}}개의 아이템",
+    "numItems_plural": "{{number}}개의 아이템",
+    "off": "끄기",
+    "openCompanionWindow_annotations": "주석",
+    "openCompanionWindow_attribution": "권리",
+    "openCompanionWindow_canvas": "인덱스",
+    "openCompanionWindow_info": "정보",
+    "openCompanionWindow_layers": "레이어",
+    "openCompanionWindow_search": "검색",
+    "openInCompanionWindow": "별도의 패널에서 열기",
+    "openWindows": "현재 열려있는 윈도우",
+    "pagination": "{{current}} of {{total}}",
+    "position": "배치",
+    "previewWindowTitle": "{{title}}",
+    "previousCanvas": "이전 아이템",
+    "related": "관련",
+    "resource": "리소스",
+    "retry": "재시도",
+    "right": "오른쪽",
+    "rights": "라이선스",
+    "scroll": "스크롤",
+    "searchInputLabel": "용어 찾기",
+    "searchNextResult": "다음 결과",
+    "searchNoResults": "해당 결과 없음",
+    "searchPreviousResult": "이전 결과",
+    "searchResultsRemaining": "{{numLeft}}개 남음",
+    "searchSubmitAria": "검색하기",
+    "searchTitle": "검색",
+    "selectWorkspaceMenu": "작업공간 유형 선택",
+    "showingNumAnnotations": "{{number}}개의 주석 나타내기",
+    "showingNumAnnotations_plural": "{{number}}개의 주석 나타내기",
+    "showCollection": "컬렉션 보이기",
+    "showZoomControls": "확대/축소 기능 보이기",
+    "sidebarPanelsNavigation": "사이드바 패널 탐색",
+    "single": "한 개",
+    "startHere": "시작하기",
+    "suggestSearch": "이 문서를 \"{{ query }}\"로 검색하기",
+    "tableOfContentsList": "목차",
+    "theme": "테마",
+    "thumbnailList": "썸네일 목록",
+    "thumbnailNavigation": "썸네일",
+    "thumbnails": "썸네일",
+    "toggleWindowSideBar": "사이드바 전환",
+    "totalCollections": "{{count}}개의 컬렉션",
+    "totalCollections_plural": "{{count}}개의 컬렉션",
+    "totalManifests": "{{count}}개의 매니페스트",
+    "totalManifests_plural": "{{count}}개의 매니페스트",
+    "tryAgain": "다시 시도하세요",
+    "untitled": "[타이틀 없음]",
+    "view": "뷰",
+    "viewWorkspaceConfiguration": "작업공간 환경설정 ㅂ괴",
+    "welcome": "Mirador에 오신 것을 환영합니다",
+    "window": "윈도우: {{label}}",
+    "windowMenu": "윈도우 뷰 & 썸네일 표시",
+    "windowNavigation": "윈도우 탐색",
+    "windowPluginButtons": "옵션",
+    "windowPluginMenu": "윈도우 옵션",
+    "workspace": "작업공간",
+    "workspaceNavigation": "작업공간 탐색",
+    "workspaceFullScreen": "전체화면",
+    "workspaceMenu": "작업공간 설정",
+    "workspaceOptions": "작업공간 옵션",
+    "workspaceSelectionTitle": "작업공간 유형을 선택하세요",
+    "zoomIn": "확대",
+    "zoomOut": "축소",
+    "zoomReset": "줌 재설정"
+  }
+}
diff --git a/src/locales/lt/translation.json b/src/locales/lt/translation.json
index 7feafb80f18ccadd293db80b7ad24e6bd2fe2e55..3955a9d5b586e4ee3f7e2ab9cdce3f114b08bc4c 100644
--- a/src/locales/lt/translation.json
+++ b/src/locales/lt/translation.json
@@ -25,7 +25,7 @@
     "closeAddResourceMenu": "Uždaryti šaltinių sąrašą",
     "closeCompanionWindow": "Uždaryti panelę",
     "closeWindow": "Uždaryti langą",
-    "collapseSection": "Suskleisti {{section}} sekciją",
+    "collapseSection": "Suskleisti \"{{section}}\" sekciją",
     "collapseSidePanel": "Suskleisti šoninę juostą",
     "itemList": "Įrašų sąrašas",
     "continue": "Tęsti",
@@ -48,7 +48,7 @@
     "errorDialogConfirm": "Gerai",
     "errorDialogTitle": "Įvyko klaida",
     "exitFullScreen": "Išjungti pilno ekrano režimą",
-    "expandSection": "Išplėsti {{section}} sekciją",
+    "expandSection": "Išplėsti \"{{section}}\" sekciją",
     "expandSidePanel": "Išplėsti šoninę juostą",
     "exportCopied": "Darbalaukio nustatymai nukopijuoti",
     "fetchManifest": "Pridėti",
diff --git a/src/locales/nbNo/translation.json b/src/locales/nbNo/translation.json
new file mode 100644
index 0000000000000000000000000000000000000000..c3118f0b87e3406b0e3fc8f81d80d188bdb46735
--- /dev/null
+++ b/src/locales/nbNo/translation.json
@@ -0,0 +1,163 @@
+{
+  "translation": {
+    "aboutMirador": "Om Projekt Mirador",
+    "aboutThisItem": "Om dette objektet",
+    "addedFromUrl": "(Lagt til fra URL)",
+    "addManifestUrl": "Nettadresse til samling eller manifest",
+    "addManifestUrlHelp": "URL til en IIIF-ressurs",
+    "addResource": "Legg til ressurs",
+    "annotationCanvasLabel_1/1": "Objekt: [{{label}}]",
+    "annotationCanvasLabel_1/2": "Venstre: [{{label}}]",
+    "annotationCanvasLabel_2/2": "Høyre: [{{label}}]",
+    "annotations": "Annoteringer",
+    "attribution": "Tilskrivelse",
+    "attributionTitle": "Rettigheter",
+    "authenticationFailed": "Autentiseringen feilet.",
+    "authenticationRequired": "Autentisering kreves for full tilgang",
+    "backToResults": "Tilbake til resultat",
+    "book": "Bok",
+    "bottom": "Nederst",
+    "cancel": "Avbryt",
+    "canvasIndex": "Index",
+    "changeTheme": "Skift tema",
+    "clearSearch": "Fjern søket",
+    "close": "Lukk",
+    "closeAddResourceForm": "Lukk skjemaet",
+    "closeAddResourceMenu": "Lukk ressurslisten",
+    "closeCompanionWindow": "Lukk panelet",
+    "closeWindow": "Lukk vinduet",
+    "collapseSection": "Lukk seksjonen \"{{section}}\"",
+    "collapseSidePanel": "Lukk sidemenyen",
+    "collection": "Samling",
+    "itemList": "Objektliste",
+    "continue": "Fortsett",
+    "copy": "Kopiere",
+    "currentItem": "Valgt objekt",
+    "currentItem_1/1": "Valgt objekt",
+    "currentItem_1/2": "Venstre",
+    "currentItem_2/2": "Høyre",
+    "dark": "Mørkt tema",
+    "digitizedView": "Digitalisert visning",
+    "dismiss": "Lukk",
+    "highlightAllAnnotations": "Markér alle",
+    "displayNoAnnotations": "Avmarkér alle",
+    "downloadExport": "Eksportér arbeidsområde",
+    "downloadExportWorkspace": "Eksportér arbeidsområde",
+    "elastic": "Elastisk",
+    "elasticDescription": "Flytt og endre størrelsen på vinduet fritt i et ubegrenset arbeidsområde. Vindu kan overlappe.",
+    "emptyResourceList": "Din resursliste er tom",
+    "error": "Error",
+    "errorDialogConfirm": "OK",
+    "errorDialogTitle": "Et problem oppstod",
+    "exitFullScreen": "Forlat fullskjermsvisning",
+    "expandSection": "Ekspandér seksjonen \"{{section}}\"",
+    "expandSidePanel": "Ekspandér sidemenyen",
+    "exportCopied": "Konfiguration av arbeidsområdet ble kopiert til din utklippstavle",
+    "fetchManifest": "Legg til",
+    "fullScreen": "Fullskjermsvisning",
+    "gallery": "Galleri",
+    "hideZoomControls": "Skjul zoomkontroll",
+    "iiif_homepage": "Om denne ressursen",
+    "iiif_manifest": "IIIF manifest",
+    "iiif_renderings": "Alternativt format",
+    "iiif_seeAlso": "Se også",
+    "import": "Importér",
+    "importWorkspace": "Importér arbeidsområde",
+    "importWorkspaceHint": "Lim inn en Mirador 3 konfigurasjon for import",
+    "item": "Objekt: {{label}}",
+    "jsError": "Tekniske detaljer",
+    "jsStack": "{{ stack }}",
+    "language": "Språk",
+    "layer_hide": "Skjul lag",
+    "layer_move": "Flytt lag",
+    "layer_opacity": "Lag-gjennomsiktighet",
+    "layer_show": "Vis lag",
+    "layer_moveToTop": "Flytt laget øverst",
+    "layers": "Lag",
+    "light": "Lyst tema",
+    "links": "Lenker",
+    "listAllOpenWindows": "Gå til vindu",
+    "login": "Logg inn",
+    "logout": "Logg ut",
+    "manifestError": "Ressursen kan ikke legges til:",
+    "maximizeWindow": "Maksimér vinduet",
+    "minimizeWindow": "Minimér vinduet",
+    "mirador": "Mirador",
+    "miradorResources": "Miradorressurser",
+    "miradorViewer": "Mirador bildeviser",
+    "more": "mer...",
+    "moreResults": "Flere resultat",
+    "mosaic": "Mosaik",
+    "mosaicDescription": "Flytt og endre størrelse på vinduet i relasjon til hverandre, innenfor den synlige rammen.",
+    "moveCompanionWindowToBottom": "Flytt til bunnen",
+    "moveCompanionWindowToRight": "Flytt til høyre",
+    "nextCanvas": "Neste objekt",
+    "noItemSelected": "Ingen valgte objekt",
+    "numItems": "{{number}} objekt",
+    "numItems_plural": "{{number}} objekter",
+    "off": "Av",
+    "openCompanionWindow_annotations": "Annoteringer",
+    "openCompanionWindow_attribution": "Rettigheter",
+    "openCompanionWindow_canvas": "Indeks",
+    "openCompanionWindow_info": "Informasjon",
+    "openCompanionWindow_layers": "Lag",
+    "openCompanionWindow_search": "Søk",
+    "openInCompanionWindow": "Åpne i eget panel",
+    "openWindows": "Åpne vindu",
+    "pagination": "{{current}} av {{total}}",
+    "position": "Posisjon",
+    "previewWindowTitle": "{{title}}",
+    "previousCanvas": "Forrige objekt",
+    "related": "Relatert",
+    "resource": "Ressurs",
+    "retry": "Forsøk igjen",
+    "right": "Til høyre",
+    "rights": "Lisens",
+    "scroll": "Bla",
+    "searchInputLabel": "Søkeord",
+    "searchNextResult": "Neste resultat",
+    "searchNoResults": "Ingen treff",
+    "searchPreviousResult": "Forrige resultat",
+    "searchResultsRemaining": "{{numLeft}} igjen",
+    "searchSubmitAria": "Søk",
+    "searchTitle": "Søk",
+    "selectWorkspaceMenu": "Velg arbeidsområde-type",
+    "showingNumAnnotations": "Vis {{number}} annotasjon",
+    "showingNumAnnotations_plural": "Vis {{number}} annotasjoner",
+    "showCollection": "Vis samling",
+    "showZoomControls": "Vis zoomkontroll",
+    "sidebarPanelsNavigation": "Sidemeny-panel navigering",
+    "single": "En og en",
+    "startHere": "Start her",
+    "suggestSearch": "Søk etter \"{{ query }}\" i dette dokument",
+    "tableOfContentsList": "Innholdsfortegnelse",
+    "theme": "Tema",
+    "thumbnailList": "Miniatyrliste",
+    "thumbnailNavigation": "Miniatyrer",
+    "thumbnails": "Miniatyrer",
+    "toggleWindowSideBar": "Vis/skjul sidemenyen",
+    "totalCollections": "{{count}} samling",
+    "totalCollections_plural": "{{count}} samlinger",
+    "totalManifests": "{{count}} manifest",
+    "totalManifests_plural": "{{count}} manifester",
+    "tryAgain": "Forsøk igjen",
+    "untitled": "[uten tittel]",
+    "view": "Visning",
+    "viewWorkspaceConfiguration": "Vis konfigurasjon av arbeidsområde.",
+    "welcome": "Velkommen til Mirador",
+    "window": "Vindu: {{label}}",
+    "windowMenu": "Vindusvisning & miniatyrvisning",
+    "windowNavigation": "Vindusnavigasjon",
+    "windowPluginButtons": "Innstillinger",
+    "windowPluginMenu": "Vindusinnstillinger",
+    "workspace": "Arbeidsområde",
+    "workspaceNavigation": "Navigasjon i arbeidsområdet",
+    "workspaceFullScreen": "Fullskjermsvisning",
+    "workspaceMenu": "Innstillinger for arbeidsområdet",
+    "workspaceOptions": "Flere valg for arbeidsområdet",
+    "workspaceSelectionTitle": "Velg arbeidsområde-type",
+    "zoomIn": "Zoom inn",
+    "zoomOut": "Zoom ut",
+    "zoomReset": "Tilbakestill zoom"
+  }
+}
diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json
index 63c1f6b2b02634611d35955f32149a1c5c62e12c..80d6e370c3d090b6b92870600d6f2e3406abaa47 100644
--- a/src/locales/nl/translation.json
+++ b/src/locales/nl/translation.json
@@ -24,7 +24,7 @@
     "closeAddResourceMenu": "Sluit lijst met bronnen",
     "closeCompanionWindow": "Sluit paneel",
     "closeWindow": "Sluit venster",
-    "collapseSection": "Klap {{section}} sectie in",
+    "collapseSection": "Klap \"{{section}}\" sectie in",
     "collapseSidePanel": "Klap zijbalk in",
     "itemList": "Compacte lijst",
     "continue": "Ga verder",
@@ -44,7 +44,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Er is een fout opgetreden",
     "exitFullScreen": "Verlaat volledig scherm",
-    "expandSection": "Klap {{section}} sectie uit",
+    "expandSection": "Klap \"{{section}}\" sectie uit",
     "expandSidePanel": "Klap zijbalk uit",
     "fetchManifest": "Voeg toe",
     "fullScreen": "Volledig scherm",
@@ -76,7 +76,8 @@
     "moveCompanionWindowToBottom": "Verplaats naar beneden",
     "moveCompanionWindowToRight": "Verplaats naar rechts",
     "nextCanvas": "Volgend item",
-    "numItems": "{{number}} items",
+    "numItems": "{{number}} item",
+    "numItems_plural": "{{number}} items", 
     "off": "Uit",
     "openCompanionWindow_annotations": "Annotaties",
     "openCompanionWindow_attribution": "Rechten",
@@ -101,7 +102,8 @@
     "searchSubmitAria": "Zoeken",
     "searchTitle": "Zoek",
     "selectWorkspaceMenu": "Selecteer workspacetype",
-    "showingNumAnnotations": "{{number}} annotaties weergegeven",
+    "showingNumAnnotations": "{{number}} annotatie weergegeven",
+    "showingNumAnnotations_plural": "{{number}} annotaties weergegeven",
     "showZoomControls": "Toon zoomknoppen",
     "sidebarPanelsNavigation": "Zijbalk panelen navigatie",
     "single": "Enkel",
@@ -112,6 +114,10 @@
     "thumbnailNavigation": "Thumbnails",
     "thumbnails": "Thumbnails",
     "toggleWindowSideBar": "Toon zijbalk",
+    "totalCollections": "{{count}} collectie",
+    "totalCollections_plural": "{{count}} collecties",
+    "totalManifests": "{{count}} manifest",
+    "totalManifests_plural": "{{count}} manifests",
     "tryAgain": "Probeer opnieuw",
     "untitled": "[Zonder titel]",
     "view": "Weergave",
diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json
new file mode 100644
index 0000000000000000000000000000000000000000..a4d3bf59adc3641afd3ba0ae6b199a9a2f0d9357
--- /dev/null
+++ b/src/locales/pl/translation.json
@@ -0,0 +1,163 @@
+{
+  "translation": {
+    "aboutMirador": "O Projekcie Mirador",
+    "aboutThisItem": "O bieżącej pozycji",
+    "addedFromUrl": "(Dodano z URL)",
+    "addManifestUrl": "Lokalizacja zasobów",
+    "addManifestUrlHelp": "URL zasobów IIIF",
+    "addResource": "Dodaj zasoby",
+    "annotationCanvasLabel_1/1": "Pozycja: [{{label}}]",
+    "annotationCanvasLabel_1/2": "Lewo: [{{label}}]",
+    "annotationCanvasLabel_2/2": "Prawo: [{{label}}]",
+    "annotations": "Adnotacje",
+    "attribution": "Atrybucja",
+    "attributionTitle": "Prawa",
+    "authenticationFailed": "Uwierzytelnianie nie powiodło się.",
+    "authenticationRequired": "Do pełnego dostępu wymagane jest uwierzytelnienie",
+    "backToResults": "Powrót do wyników",
+    "book": "Książka",
+    "bottom": "Dół",
+    "cancel": "Anuluj",
+    "canvasIndex": "Indeks",
+    "changeTheme": "Zmień motyw",
+    "clearSearch": "wyczyść",
+    "close": "Zamknij",
+    "closeAddResourceForm": "Zamknij formularz",
+    "closeAddResourceMenu": "Zamknij listę zasobów",
+    "closeCompanionWindow": "Zamknij panel",
+    "closeWindow": "Zamknij okno",
+    "collapseSection": "Zwiń sekcję \"{{section}}\"",
+    "collapseSidePanel": "Zwiń panel boczny",
+    "collection": "Zbiór",
+    "itemList": "Lista pozycji",
+    "continue": "Kontynuuj",
+    "copy": "Kopiuj",
+    "currentItem": "Bieżąca pozycja",
+    "currentItem_1/1": "Bieżąca pozycja",
+    "currentItem_1/2": "Lewo",
+    "currentItem_2/2": "Prawo",
+    "dark": "Ciemny motyw",
+    "digitizedView": "Widok zdigitalizowany",
+    "dismiss": "Odrzuć",
+    "highlightAllAnnotations": "Podświetl wszystko",
+    "displayNoAnnotations": "Nie podświetlaj",
+    "downloadExport": "Eksportuj obszar roboczy",
+    "downloadExportWorkspace": "Eksportuj obszar roboczy",
+    "elastic": "Elastyczny",
+    "elasticDescription": "Swobodnie przesuwaj i dopasowuj okna w nieograniczonej przestrzeni roboczej. Okna mogą zachodzić na siebie.",
+    "emptyResourceList": "Twoja lista zasobów jest pusta",
+    "error": "Błąd",
+    "errorDialogConfirm": "OK",
+    "errorDialogTitle": "Wystąpił błąd",
+    "exitFullScreen": "Wyłącz tryb pełnoekranowy",
+    "expandSection": "Rozwiń sekcję \"{{section}}\"",
+    "expandSidePanel": "Rozwiń panel boczny",
+    "exportCopied": "Konfiguracja obszaru roboczego została skopiowana do schowka",
+    "fetchManifest": "Dodaj",
+    "fullScreen": "Pełny ekran",
+    "gallery": "Galeria",
+    "hideZoomControls": "Ukryj kontrolki powiększenia",
+    "iiif_homepage": "O tym zasobie",
+    "iiif_manifest": "Manifest IIIF",
+    "iiif_renderings": "Alternatywne formaty",
+    "iiif_seeAlso": "Zobacz też",
+    "import" : "Importuj",
+    "importWorkspace": "Importuj obszar roboczy",
+    "importWorkspaceHint": "Wklej konfigurację Mirador 3 do zaimportowania",
+    "item": "Pozycja: {{label}}",
+    "jsError": "Szczegóły techniczne",
+    "jsStack": "{{ stack }}",
+    "language": "Język",
+    "layer_hide": "Ukryj warstwę",
+    "layer_move": "Przenieś warstwę",
+    "layer_opacity": "Krycie warstwy",
+    "layer_show": "Pokaż warstwę",
+    "layer_moveToTop": "Przenieś warstwę na górę",
+    "layers": "Warstwy",
+    "light": "Jasny motyw",
+    "links": "Linki",
+    "listAllOpenWindows": "Przejdź do okna",
+    "login": "Zaloguj",
+    "logout": "Wyloguj",
+    "manifestError": "Następujące zasoby nie mogą być dodane:",
+    "maximizeWindow": "Maksymalizuj okno",
+    "minimizeWindow": "Minimalizuj okno",
+    "mirador": "Mirador",
+    "miradorResources": "Zasoby Mirador",
+    "miradorViewer": "Przeglądarka Mirador",
+    "more": "więcej...",
+    "moreResults": "Więcej wyników",
+    "mosaic": "Mozaika",
+    "mosaicDescription": "Przesuwaj i zmieniaj rozmiary okien względem siebie, w widocznej ramce.",
+    "moveCompanionWindowToBottom": "Przesuń na dół",
+    "moveCompanionWindowToRight": "Przesuń w prawo",
+    "nextCanvas": "Następna pozycja",
+    "noItemSelected": "Nie wybrano pozycji",
+    "numItems": "{{number}} pozycja",
+    "numItems_plural": "{{number}} pozycje",
+    "off": "Wyłącz",
+    "openCompanionWindow_annotations": "Adnotacje",
+    "openCompanionWindow_attribution": "Prawa",
+    "openCompanionWindow_canvas": "Indeks",
+    "openCompanionWindow_info": "Informacje",
+    "openCompanionWindow_layers": "Warstwy",
+    "openCompanionWindow_search": "Szukaj",
+    "openInCompanionWindow": "Otwórz w oddzielnym panelu",
+    "openWindows": "Aktualnie otwarte okna",
+    "pagination": "{{current}} of {{total}}",
+    "position": "Pozycja",
+    "previewWindowTitle": "{{title}}",
+    "previousCanvas": "Poprzednia pozycja",
+    "related": "Powiązane",
+    "resource": "Zasoby",
+    "retry": "Spróbuj ponownie",
+    "right": "Prawo",
+    "rights": "Licencja",
+    "scroll": "Przewiń",
+    "searchInputLabel": "szukane słowa",
+    "searchNextResult": "Następny wynik",
+    "searchNoResults": "Nie znaleziono wyników",
+    "searchPreviousResult": "Poprzedni wynik",
+    "searchResultsRemaining": "Pozostało {{numLeft}}",
+    "searchSubmitAria": "Wyszukaj",
+    "searchTitle": "Wyszukaj",
+    "selectWorkspaceMenu": "Wybierz typ obszaru roboczego",
+    "showingNumAnnotations": "Wyświetlanie {{number}} adnotacji",
+    "showingNumAnnotations_plural": "Wyświetlanie {{number}} adnotacji",
+    "showCollection": "Pokaż zbiór",
+    "showZoomControls": "Pokaż kontrolki powiększenia",
+    "sidebarPanelsNavigation": "Nawigacja pasków panelu bocznego",
+    "single": "Pojedynczy",
+    "startHere": "Zacznij tutaj",
+    "suggestSearch": "Przeszukaj ten dokument pod kątem \"{{ query }}\"",
+    "tableOfContentsList": "Spis treści",
+    "theme": "Motyw",
+    "thumbnailList": "Lista miniatur",
+    "thumbnailNavigation": "Miniatury",
+    "thumbnails": "Miniatury",
+    "toggleWindowSideBar": "Przełącz panel boczny",
+    "totalCollections": "{{count}} zbiór",
+    "totalCollections_plural": "{{count}} zbiorów",
+    "totalManifests": "{{count}} manifest",
+    "totalManifests_plural": "{{count}} manifestów",
+    "tryAgain": "Spróbuj ponownie",
+    "untitled": "[Bez nazwy]",
+    "view": "Widok",
+    "viewWorkspaceConfiguration": "Wyświetl konfigurację obszaru roboczego",
+    "welcome": "Witaj w Miradorze",
+    "window": "Okno: {{label}}",
+    "windowMenu": "Widoki okien i wyświetlanie miniatur",
+    "windowNavigation": "Nawigacja okna",
+    "windowPluginButtons": "Opcje",
+    "windowPluginMenu": "Opcje okna",
+    "workspace": "Obszar roboczy",
+    "workspaceNavigation": "Nawigacja obszaru roboczego",
+    "workspaceFullScreen": "Pełny ekran",
+    "workspaceMenu": "Ustawienia obszaru roboczego",
+    "workspaceOptions": "Opcje obszaru roboczego",
+    "workspaceSelectionTitle": "Wybierz typ obszaru roboczego",
+    "zoomIn": "Przybliż",
+    "zoomOut": "Oddal",
+    "zoomReset": "Zresetuj powiększenie"
+  }
+}
diff --git a/src/locales/ptBr/translation.json b/src/locales/ptBr/translation.json
index b258baf177d015bc1250604982bce171d9172224..bbf06666a8019f7beb3822e6a5fdc3cf1c69de55 100644
--- a/src/locales/ptBr/translation.json
+++ b/src/locales/ptBr/translation.json
@@ -24,7 +24,7 @@
     "closeAddResourceMenu": "Fechar lista de conteúdo",
     "closeCompanionWindow": "Fechar painel",
     "closeWindow": "Fechar janela",
-    "collapseSection": "Suprimir seção {{section}}",
+    "collapseSection": "Suprimir seção \"{{section}}\"",
     "collapseSidePanel": "Suprimir barra lateral",
     "itemList": "Lista compacta",
     "continue": "Continuar",
@@ -44,7 +44,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Um erro ocorreu",
     "exitFullScreen": "Sair do modo tela cheia",
-    "expandSection": "Expandir seção {{section}}",
+    "expandSection": "Expandir seção \"{{section}}\"",
     "expandSidePanel": "Expandir barra lateral",
     "fetchManifest": "Adicionar",
     "fullScreen": "Tela cheia",
diff --git a/src/locales/sr/translation.json b/src/locales/sr/translation.json
index 1e60143ccbe7e8d695dc24e4bf808726282676d3..032e40fb612cacaa8ea774feff6dab9b914fd08f 100644
--- a/src/locales/sr/translation.json
+++ b/src/locales/sr/translation.json
@@ -26,7 +26,7 @@
     "closeAddResourceMenu": "Затворите листу ресурса",
     "closeCompanionWindow": "Затворите панел",
     "closeWindow": "Затворите приказ",
-    "collapseSection": "Сакријте {{section}} секцију",
+    "collapseSection": "Сакријте \"{{section}}\" секцију",
     "collapseSidePanel": "Сакријте",
     "collection": "Колекција",
     "itemList": "Листа страница",
@@ -50,7 +50,7 @@
     "errorDialogConfirm": "OK",
     "errorDialogTitle": "Дошло је до грешке",
     "exitFullScreen": "Изађите из приказа преко целог екрана",
-    "expandSection": "Проширите {{section}} секцију",
+    "expandSection": "Проширите \"{{section}}\" секцију",
     "expandSidePanel": "Прикажите",
     "exportCopied": "Конфигурација радног окружења је копирана у привремену меморију",
     "fetchManifest": "Додајте",
diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json
new file mode 100644
index 0000000000000000000000000000000000000000..866d0c5e5f2250ab9efa0a9264a9f4ba66e6829c
--- /dev/null
+++ b/src/locales/sv/translation.json
@@ -0,0 +1,158 @@
+{
+  "translation": {
+    "aboutMirador": "Om Projekt Mirador",
+    "aboutThisItem": "Om det här objektet",
+    "addedFromUrl": "(Tillagd från URL)",
+    "addManifestUrl": "Webbadress till samling eller manifest",
+    "addManifestUrlHelp": "URL till en IIIF-resurs",
+    "addResource": "Lägg till resurs",
+    "annotationCanvasLabel_1/1": "Objekt: [{{label}}]",
+    "annotationCanvasLabel_1/2": "Vänster: [{{label}}]",
+    "annotationCanvasLabel_2/2": "Höger: [{{label}}]",
+    "annotations": "Noteringar",
+    "attribution": "Tillskrivning",
+    "attributionTitle": "Rättigheter",
+    "authenticationFailed": "Autentisering misslyckades.",
+    "authenticationRequired": "Autentisering krävs för full åtkomst",
+    "backToResults": "Tillbaka till resultat",
+    "book": "Bok",
+    "bottom": "Nederkant",
+    "cancel": "Avbryt",
+    "canvasIndex": "Index",
+    "changeTheme": "Ändra tema",
+    "clearSearch": "Ta bort sökning",
+    "close": "Stäng",
+    "closeAddResourceForm": "Stäng formulär",
+    "closeAddResourceMenu": "Stäng resurslista",
+    "closeCompanionWindow": "Stäng panel",
+    "closeWindow": "Stäng fönster",
+    "collapseSection": "Stäng sektionen \"{{section}}\"",
+    "collapseSidePanel": "Stäng sidofält",
+    "collection": "Samling",
+    "itemList": "Objektlista",
+    "continue": "Fortsätt",
+    "copy": "Kopiera",
+    "currentItem": "Aktuellt objekt",
+    "currentItem_1/1": "Aktuellt objekt",
+    "currentItem_1/2": "Vänster",
+    "currentItem_2/2": "Höger",
+    "dark": "Mörkt tema",
+    "digitizedView": "Digitaliserad vy",
+    "dismiss": "Stäng",
+    "highlightAllAnnotations": "Markera alla",
+    "displayNoAnnotations": "Avmarkera alla",
+    "downloadExport": "Exportera arbetsyta",
+    "downloadExportWorkspace": "Exportera arbetsyta",
+    "elastic": "Elastisk",
+    "elasticDescription": "Flytta och ändra storlek på fönster fritt i en obegränsad arbetsyta. Fönster kan överlappa.",
+    "emptyResourceList": "Din resurslista är tom",
+    "error": "Error",
+    "errorDialogConfirm": "OK",
+    "errorDialogTitle": "Ett problem uppstod",
+    "exitFullScreen": "Lämna helskärmsläge",
+    "expandSection": "Expandera sektionen \"{{section}}\"",
+    "expandSidePanel": "Expandera sidofält",
+    "exportCopied": "Konfiguration av arbetsytan har kopierats till dina urklipp",
+    "fetchManifest": "Lägg till",
+    "fullScreen": "Helskärmsläge",
+    "gallery": "Galleri",
+    "hideZoomControls": "Dölj zoomkontroller",
+    "iiif_homepage": "Om den här resursen",
+    "iiif_manifest": "IIIF manifest",
+    "iiif_renderings": "Alternativa format",
+    "iiif_seeAlso": "Se även",
+    "import" : "Importera",
+    "importWorkspace": "Importera arbetsyta",
+    "importWorkspaceHint": "Klistra in en Mirador 3 konfiguration att importera",
+    "item": "Objekt: {{label}}",
+    "jsError": "Tekniska detaljer",
+    "jsStack": "{{ stack }}",
+    "language": "Språk",
+    "layer_hide": "Dölj lager",
+    "layer_move": "Flytta lager",
+    "layer_opacity": "Lageropacitet",
+    "layer_show": "Visa lager",
+    "layer_moveToTop": "Flytta lager till toppen",
+    "layers": "Lager",
+    "light": "Ljust tema",
+    "links": "Länkar",
+    "listAllOpenWindows": "Gå till fönster",
+    "login": "Logga in",
+    "logout": "Logga ut",
+    "manifestError": "Resursen kan inte läggas till:",
+    "maximizeWindow": "Maximera fönster",
+    "minimizeWindow": "Minimera fönster",
+    "mirador": "Mirador",
+    "miradorResources": "Miradorresurser",
+    "miradorViewer": "Mirador bildvisare",
+    "more": "mer...",
+    "moreResults": "Fler resultat",
+    "mosaic": "Mosaik",
+    "mosaicDescription": "Flytta och ändra storlek på fönster i relation till varandra, innanför den synliga ramen.",
+    "moveCompanionWindowToBottom": "Flytta till botten",
+    "moveCompanionWindowToRight": "Flytta till höger",
+    "nextCanvas": "Nästa objekt",
+    "noItemSelected": "Inga valda objekt",
+    "numItems": "{{number}} objekt",
+    "off": "Av",
+    "openCompanionWindow_annotations": "Noteringar",
+    "openCompanionWindow_attribution": "Rättigheter",
+    "openCompanionWindow_canvas": "Index",
+    "openCompanionWindow_info": "Information",
+    "openCompanionWindow_layers": "Lager",
+    "openCompanionWindow_search": "Sök",
+    "openInCompanionWindow": "Öppna i separat panel",
+    "openWindows": "Öppna fönster",
+    "pagination": "{{current}} av {{total}}",
+    "position": "Position",
+    "previewWindowTitle": "{{title}}",
+    "previousCanvas": "Föregående objekt",
+    "related": "Relaterat",
+    "resource": "Resurs",
+    "retry": "Försök igen",
+    "right": "Till höger",
+    "rights": "Licens",
+    "scroll": "Scrolla",
+    "searchInputLabel": "Sökord",
+    "searchNextResult": "Nästa resultat",
+    "searchNoResults": "Inga resultat hittades",
+    "searchPreviousResult": "Föregående resultat",
+    "searchResultsRemaining": "{{numLeft}} kvar",
+    "searchSubmitAria": "Sök",
+    "searchTitle": "Sök",
+    "selectWorkspaceMenu": "Välj typ av arbetsyta",
+    "showingNumAnnotations": "Visar {{number}} noteringar",
+    "showCollection": "Visa samling",
+    "showZoomControls": "Visa zoomkontroller",
+    "sidebarPanelsNavigation": "Sidofältspaneler navigering",
+    "single": "En och en",
+    "startHere": "Börja här",
+    "suggestSearch": "Sök i detta dokument efter \"{{ query }}\"",
+    "tableOfContentsList": "Innehållsförteckning",
+    "theme": "Tema",
+    "thumbnailList": "Miniatyrlista",
+    "thumbnailNavigation": "Miniatyrer",
+    "thumbnails": "Miniatyrer",
+    "toggleWindowSideBar": "Visa/dölj sidofält",
+    "totalCollections": "{{count}} samlingar",
+    "totalManifests": "{{count}} manifest",
+    "tryAgain": "Försök igen",
+    "untitled": "[namnlös]",
+    "view": "Vy",
+    "welcome": "Välkommen till Mirador",
+    "window": "Fönster: {{label}}",
+    "windowMenu": "Fönstervyer & miniatyrdisplay",
+    "windowNavigation": "Fönsternavigation",
+    "windowPluginButtons": "Inställningar",
+    "windowPluginMenu": "Fönsterinställningar",
+    "workspace": "Arbetsyta",
+    "workspaceNavigation": "Navigera i arbetsyta",
+    "workspaceFullScreen": "Helskärmsläge",
+    "workspaceMenu": "Inställningar för arbetsyta",
+    "workspaceOptions": "Fler val för arbetsyta",
+    "workspaceSelectionTitle": "Välj typ av arbetsyta",
+    "zoomIn": "Zooma in",
+    "zoomOut": "Zooma ut",
+    "zoomReset": "Återställ zoom"
+  }
+}
diff --git a/src/locales/vi/translation.json b/src/locales/vi/translation.json
new file mode 100644
index 0000000000000000000000000000000000000000..9429e5ece7cda375d6e68ad6c92950634c042be5
--- /dev/null
+++ b/src/locales/vi/translation.json
@@ -0,0 +1,157 @@
+{
+  "translation": {
+    "aboutMirador": "Về dự án Mirador",
+    "aboutThisItem": "Về khoản mục này",
+    "addedFromUrl": "(Được thêm từ URL)",
+    "addManifestUrl": "Vị trí tài nguyên",
+    "addManifestUrlHelp": "URL của tài nguyên IIIF",
+    "addResource": "Bổ sung tài nguyên",
+    "annotationCanvasLabel_1/1": "Khoản mục: [{{label}}]",
+    "annotationCanvasLabel_1/2": "Trái: [{{label}}]",
+    "annotationCanvasLabel_2/2": "Phải: [{{label}}]",
+    "annotations": "Chú giải",
+    "attribution": "Quyền hạn",
+    "attributionTitle": "Quyền",
+    "authenticationFailed": "Xác thực thất bại.",
+    "authenticationRequired": "Xác thực được yêu cầu cho truy nhập đầy đủ",
+    "backToResults": "Trở lại kết quả",
+    "book": "Sách",
+    "bottom": "Đáy",
+    "cancel": "Huỷ bỏ",
+    "canvasIndex": "Chỉ mục",
+    "changeTheme": "Đổi chủ đề",
+    "clearSearch": "xoá",
+    "close": "Đóng",
+    "closeAddResourceForm": "Đóng mẫu",
+    "closeAddResourceMenu": "Đóng danh sách tài nguyên",
+    "closeCompanionWindow": "Đóng panel",
+    "closeWindow": "Đóng cửa sổ",
+    "collapseSection": "Co sập {{section}} mục",
+    "collapseSidePanel": "Co sập thanh bên",
+    "collection": "Tuyển tập",
+    "itemList": "Danh sách khoản mục",
+    "continue": "Tiếp tục",
+    "copy": "Sao",
+    "currentItem": "Khoản mục hiện thời",
+    "currentItem_1/1": "Khoản mục hiện thời",
+    "currentItem_1/2": "Trái",
+    "currentItem_2/2": "Phải",
+    "dark": "Chủ đề tối",
+    "digitizedView": "Cái nhìn số hoá",
+    "dismiss": "Bác bỏ",
+    "highlightAllAnnotations": "Làm nổi bật tất",
+    "displayNoAnnotations": "Không làm nổi bật",
+    "downloadExport": "Vùng xuất khẩu",
+    "downloadExportWorkspace": "Vùng xuất khẩu",
+    "elastic": "Co giãn",
+    "elasticDescription": "Di chuyển và định cỡ cửa sổ tự do trong vùng vô giới hạn. Cửa sổ có thể chèn lấp.",
+    "emptyResourceList": "Danh sách tài nguyên của bạn là trống",
+    "error": "Lỗi",
+    "errorDialogConfirm": "OK",
+    "errorDialogTitle": "Lỗi đã xuất hiện",
+    "exitFullScreen": "Ra khỏi toàn màn hình",
+    "expandSection": "Mở rộng {{section}} mục",
+    "expandSidePanel": "Mở rộng thanh bên",
+    "exportCopied": "Cấu hình vùng làm việc được sao vào bảng đệm của bạn",
+    "fetchManifest": "Thêm",
+    "fullScreen": "Toàn màn hình",
+    "gallery": "Phòng tranh",
+    "hideZoomControls": "Ẩn điều khiển thu phóng",
+    "iiif_homepage": "Về tài nguyên này",
+    "iiif_manifest": "Bản kê IIIF",
+    "iiif_renderings": "Dạng thức luân phiên",
+    "iiif_seeAlso": "Cũng xem",
+    "import" : "Nhập khẩu",
+    "importWorkspace": "Vùng nhập khẩu",
+    "importWorkspaceHint": "Dán cấu hình Mirador 3 để được nhập khẩu",
+    "item": "Khoản mục: {{label}}",
+    "jsError": "Chi tiết kĩ thuật",
+    "jsStack": "{{ stack }}",
+    "language": "Ngôn ngữ",
+    "layer_hide": "Giấu tầng",
+    "layer_move": "Chuyển tầng",
+    "layer_opacity": "Làm mờ tầng",
+    "layer_show": "Hiện tầng",
+    "layer_moveToTop": "Chuyển tầng lên đỉnh",
+    "layers": "Tầng",
+    "light": "Chủ đề sáng",
+    "links": "Móc nối",
+    "listAllOpenWindows": "Nhảy tới cửa sổ",
+    "login": "Đăng nhập",
+    "logout": "Đăng xuất",
+    "manifestError": "Tài nguyên không thể được bổ sung:",
+    "maximizeWindow": "Cực đại cửa sổ",
+    "minimizeWindow": "Cực tiểu cửa sổ",
+    "mirador": "Mirador",
+    "miradorResources": "Tài nguyên Mirador",
+    "miradorViewer": "Bộ xem Mirador",
+    "more": "thêm...",
+    "moreResults": "Thêm kết quả",
+    "mosaic": "Mosaic",
+    "mosaicDescription": "Di chuyển và định cỡ cửa sổ trong quan hệ lẫn nhau, bên trong khung thấy được.",
+    "moveCompanionWindowToBottom": "Chuyển tới đáy",
+    "moveCompanionWindowToRight": "Chuyển sang phải",
+    "nextCanvas": "Khoản mục tiếp",
+    "noItemSelected": "Không khoản mục nào được chọn",
+    "numItems": "{{number}} khoản mục",
+    "off": "Off",
+    "openCompanionWindow_annotations": "Chú giải",
+    "openCompanionWindow_attribution": "Quyền",
+    "openCompanionWindow_canvas": "Chỉ mục",
+    "openCompanionWindow_info": "Thông tin",
+    "openCompanionWindow_layers": "Tầng",
+    "openCompanionWindow_search": "Tìm",
+    "openInCompanionWindow": "Mở trong ngăn tách rời",
+    "openWindows": "Cửa sổ mở hiện thời",
+    "pagination": "{{current}} trong {{total}}",
+    "position": "Vị trí",
+    "previewWindowTitle": "{{title}}",
+    "previousCanvas": "Khoản mục trước",
+    "related": "Có liên quan",
+    "resource": "Tài nguyên",
+    "retry": "Thử lại",
+    "right": "Quyền",
+    "rights": "Cấp phép",
+    "scroll": "Cuộn",
+    "searchInputLabel": "Từ tìm kiếm",
+    "searchNextResult": "Kết quả tiếp",
+    "searchNoResults": "Không tìm được kết quả nào",
+    "searchPreviousResult": "Kết quả trước",
+    "searchSubmitAria": "Đệ trình việc tìm",
+    "searchTitle": "Tìm",
+    "selectWorkspaceMenu": "Chọn kiểu vùng làm việc",
+    "showingNumAnnotations": "Hiện {{number}} chú giải",
+    "showCollection": "Hiện bộ sưu tập",
+    "showZoomControls": "Hiện kiểm soát thu phóng",
+    "sidebarPanelsNavigation": "Dẫn lái ngăn thanh bên",
+    "single": "Chỉ một",
+    "startHere": "Bắt đầu ở đây",
+    "suggestSearch": "Tìm tài liệu này cho \"{{ query }}\"",
+    "tableOfContentsList": "Mục lục",
+    "theme": "Chủ đề",
+    "thumbnailList": "Danh sách ảnh thu nhỏ",
+    "thumbnailNavigation": "Ảnh thu nhỏ",
+    "thumbnails": "Ảnh thu nhỏ",
+    "toggleWindowSideBar": "chốt thanh bên",
+    "totalCollections": "{{count}} bộ sưu tập",
+    "totalManifests": "{{count}} bản kê",
+    "tryAgain": "Thử lại",
+    "untitled": "[Untitled]",
+    "view": "Xem",
+    "welcome": "Chào mừng bạn tới Mirador",
+    "window": "Cửa sổ: {{label}}",
+    "windowMenu": "Xem cửa sổ & hiển thị ảnh thu nhỏ",
+    "windowNavigation": "Dẫn lái cửa sổ",
+    "windowPluginButtons": "Tuỳ chọn",
+    "windowPluginMenu": "Tuỳ chọn cửa sổ",
+    "workspace": "Vùng làm việc",
+    "workspaceNavigation": "Dẫn lái vùng làm việc",
+    "workspaceFullScreen": "Toàn màn hình",
+    "workspaceMenu": "Thiết đặt vùng làm việc",
+    "workspaceOptions": "Tuỳ chọn vùng làm việc",
+    "workspaceSelectionTitle": "Chọn kiểu vùng làm việc",
+    "zoomIn": "Thu nhỏ",
+    "zoomOut": "Phóng to",
+    "zoomReset": "Đặt lại thu phóng"
+  }
+}
\ No newline at end of file
diff --git a/src/locales/zhCn/translation.json b/src/locales/zhCn/translation.json
index b64f7e837a984084cca9023214a95d3c74d15a45..15542807ba647e0cb8c8a4dffb66208486f128b8 100644
--- a/src/locales/zhCn/translation.json
+++ b/src/locales/zhCn/translation.json
@@ -1,116 +1,164 @@
 {
   "translation": {
-    "aboutThisItem": "有关此物件",
+    "aboutMirador": "关于Mirador项目",
+    "aboutThisItem": "有关此条目",
     "addedFromUrl": "(从URL添加)",
     "addManifestUrl": "来源",
     "addManifestUrlHelp": "IIIF资源的URL",
     "addResource": "添加资源",
-    "annotationCanvasLabel_1/1": "物件: [{{label}}]",
+    "annotationCanvasLabel_1/1": "条目: [{{label}}]",
     "annotationCanvasLabel_1/2": "左方: [{{label}}]",
     "annotationCanvasLabel_2/2": "右方: [{{label}}]",
-    "annotations": "注释",
-    "attribution": "着作权",
-    "attributionTitle": "着作权",
+    "annotations": "标注",
+    "attribution": "著作权",
+    "attributionTitle": "著作权",
+    "authenticationFailed": "认证失败。",
+    "authenticationRequired": "完全访问需要认证",
+    "backToResults": "返回到结果",
     "book": "书籍",
     "bottom": "下方",
     "cancel": "取消",
     "canvasIndex": "索引",
-    "changeTheme": "变更佈景主题",
+    "changeTheme": "变更背景主题",
+    "clearSearch": "清除",
+    "close": "关闭",
     "closeAddResourceForm": "关闭表格",
     "closeAddResourceMenu": "关闭资源列表",
-    "closeCompanionWindow": "关闭附属视窗",
-    "closeWindow": "关闭视窗",
+    "closeCompanionWindow": "关闭附属窗口",
+    "closeWindow": "关闭窗口",
     "collapseSection": "关闭{{section}}分页",
     "collapseSidePanel": "关闭边栏",
+    "collection": "集合",
     "itemList": "标题列表",
-    "copy": "複製",
-    "currentItem": "目前物件",
-    "currentItem_1/1": "目前物件",
+    "continue": "继续",
+    "copy": "复制",
+    "currentItem": "当前条目",
+    "currentItem_1/1": "当前条目",
     "currentItem_1/2": "左方",
     "currentItem_2/2": "右方",
-    "dark": "黑色主题",
+    "dark": "暗色主题",
+    "digitizedView": "数字视图",
     "dismiss": "关闭信息",
-    "highlightAllAnnotations": "显示所有注释",
-    "downloadExport": "滙出桌面排版",
-    "downloadExportWorkspace": "滙出桌面排版",
+    "highlightAllAnnotations": "高亮所有标注",
+    "displayNoAnnotations": "不高亮",
+    "downloadExport": "导出桌面排版",
+    "downloadExportWorkspace": "导出桌面排版",
     "elastic": "弹性",
-    "elasticDescription": "在桌面上自由摆放视窗",
-    "emptyResourceList": "资源列表没有物件",
+    "elasticDescription": "在桌面上自由摆放窗口",
+    "emptyResourceList": "空资源列表",
+    "error": "错误",
     "errorDialogConfirm": "确定",
     "errorDialogTitle": "发生错误",
-    "exitFullScreen": "退出全萤幕",
+    "exitFullScreen": "退出全屏",
     "expandSection": "开启{{section}}分页",
     "expandSidePanel": "开启边栏",
+    "exportCopied": "工作区配置被复制到你的剪贴板上了",
     "fetchManifest": "添加",
-    "fullScreen": "全萤幕",
-    "gallery": "矩列",
+    "fullScreen": "全屏",
+    "gallery": "画廊",
     "hideZoomControls": "隐藏缩放选项",
-    "iiif_homepage": "有关此资源",
-    "iiif_manifest": "IIIF",
+    "iiif_homepage": "主页",
+    "iiif_manifest": "IIIF清单",
     "iiif_renderings": "其他格式",
     "iiif_seeAlso": "另见",
-    "import" : "滙入",
-    "importWorkspace": "滙入桌面排版",
+    "import" : "导入",
+    "importWorkspace": "导入桌面排版",
     "importWorkspaceHint": "在此贴上Mirador 3排版设定码",
-    "item": "物件: {{label}}",
+    "item": "条目: {{label}}",
+    "jsError": "技术细节",
+    "jsStack": "{{ stack }}",
     "language": "语言",
-    "light": "白色主题",
-    "links": "连结",
-    "listAllOpenWindows": "切换至视窗",
+    "layer_hide": "隐藏图层",
+    "layer_move": "移动图层",
+    "layer_opacity": "图层不透明度",
+    "layer_show": "显示图层",
+    "layer_moveToTop": "将图层移到顶部",
+    "layers": "图层",
+    "light": "亮色主题",
+    "links": "链接",
+    "listAllOpenWindows": "切换至窗口",
     "login": "登入",
+    "logout": "登出",
     "manifestError": "无法增添资源:",
-    "maximizeWindow": "视窗最大化",
-    "minimizeWindow": "视窗最小化",
+    "maximizeWindow": "窗口最大化",
+    "minimizeWindow": "窗口最小化",
     "mirador": "Mirador",
     "miradorResources": "Mirador资源",
     "miradorViewer": "Mirador阅览器",
+    "more": "更多...",
+    "moreResults": "更多结果",
     "mosaic": "马赛克",
-    "mosaicDescription": "在桌面上以格状方式排列视窗",
+    "mosaicDescription": "在桌面上以格状方式排列窗口",
     "moveCompanionWindowToBottom": "移至下方",
     "moveCompanionWindowToRight": "移至右方",
+    "multipartCollection": "多卷集合",
     "nextCanvas": "下一页",
-    "numItems": "{{number}} 项物件",
+    "noItemSelected": "没有条目被选中",
+    "numItems": "{{number}} 项条目",
+    "numItems_plural": "{{number}} 项条目",
     "off": "关闭",
-    "openCompanionWindow_annotations": "注释",
-    "openCompanionWindow_attribution": "着作权",
+    "openCompanionWindow_annotations": "标注",
+    "openCompanionWindow_attribution": "著作权",
     "openCompanionWindow_canvas": "目录",
     "openCompanionWindow_info": "资讯",
-    "openInCompanionWindow": "移至新附属视窗",
-    "openWindows": "现有视窗",
+    "openCompanionWindow_layers": "图层",
+    "openCompanionWindow_search": "搜索",
+    "openInCompanionWindow": "移至新附属窗口",
+    "openWindows": "当前窗口",
     "pagination": "{{current}} / {{total}}",
     "position": "位置",
     "previewWindowTitle": "{{title}}",
     "previousCanvas": "上一页",
-    "related": "相关资讯",
+    "related": "相关信息",
     "resource": "资源",
+    "retry": "重试",
     "right": "右方",
     "rights": "版权",
+    "scroll": "滚动",
+    "searchInputLabel": "搜索关键字",
+    "searchNextResult": "下一个结果",
+    "searchNoResults": "没有搜索到结果",
+    "searchPreviousResult": "前一个结果",
+    "searchResultsRemaining": "{{numLeft}}剩余",
+    "searchSubmitAria": "提交搜索",
+    "searchTitle": "搜索",
     "selectWorkspaceMenu": "选择桌面排版方式",
-    "showingNumAnnotations": "显示 {{number}} 项注释",
+    "showingNumAnnotations": "显示 {{number}} 项标注",
+    "showingNumAnnotations_plural": "显示 {{number}} 项标注",
+    "showCollection": "显示集合",
     "showZoomControls": "显示缩放选项",
     "sidebarPanelsNavigation": "切换边栏",
     "single": "单项",
     "startHere": "按此开始",
-    "theme": "佈景主题",
-    "thumbnailList": "缩图列表",
-    "thumbnailNavigation": "缩图",
-    "thumbnails": "显示缩图",
+    "suggestSearch": "搜索本文档以\"{{ query }}\"",
+    "tableOfContentsList": "目录",
+    "theme": "背景主题",
+    "thumbnailList": "缩略图列表",
+    "thumbnailNavigation": "缩略图",
+    "thumbnails": "缩略图",
     "toggleWindowSideBar": "切换边栏开关",
+    "totalCollections": "{{count}} 集合",
+    "totalCollections_plural": "{{count}} 集合",
+    "totalManifests": "{{count}} 清单",
+    "totalManifests_plural": "{{count}} 清单",
     "tryAgain": "请重试",
     "untitled": "[无标题]",
-    "view": "物件排列方式",
+    "view": "条目排列方式",
+    "viewWorkspaceConfiguration": "查看工作区配置",
     "welcome": "欢迎使用Mirador",
-    "window": "视窗: {{label}}",
-    "windowMenu": "视窗选项",
-    "windowNavigation": "切换视窗",
+    "window": "窗口: {{label}}",
+    "windowMenu": "窗口视图 & 缩略图显示",
+    "windowNavigation": "切换窗口",
     "windowPluginButtons": "选项",
+    "windowPluginMenu": "窗口选项",
     "workspace": "桌面",
-    "workspaceFullScreen": "全萤幕",
+    "workspaceNavigation": "工作区导航",
+    "workspaceFullScreen": "全屏",
     "workspaceMenu": "桌面设定",
     "workspaceOptions": "桌面选项",
     "workspaceSelectionTitle": "选择桌面排版方式",
     "zoomIn": "放大",
-    "zoomOut": "放小",
+    "zoomOut": "缩小",
     "zoomReset": "重设缩放"
   }
 }
diff --git a/src/locales/zhTw/translation.json b/src/locales/zhTw/translation.json
index 0c864dd7b4374174eda25cd20ed121681941a8e7..08eff9ccc6a0da8bd81fff829bc5c09f92665553 100644
--- a/src/locales/zhTw/translation.json
+++ b/src/locales/zhTw/translation.json
@@ -1,5 +1,6 @@
 {
   "translation": {
+    "aboutMirador": "關於Mirador項目",
     "aboutThisItem": "有關此物件",
     "addedFromUrl": "(從URL添加)",
     "addManifestUrl": "來源",
@@ -8,73 +9,100 @@
     "annotationCanvasLabel_1/1": "物件: [{{label}}]",
     "annotationCanvasLabel_1/2": "左方: [{{label}}]",
     "annotationCanvasLabel_2/2": "右方: [{{label}}]",
-    "annotations": "注釋",
+    "annotations": "標註",
     "attribution": "著作權",
     "attributionTitle": "著作權",
+    "authenticationFailed": "認證失敗。",
+    "authenticationRequired": "完全訪問需要認證",
+    "backToResults": "返回到結果",
     "book": "書籍",
     "bottom": "下方",
     "cancel": "取消",
     "canvasIndex": "索引",
     "changeTheme": "變更佈景主題",
+    "clearSearch": "清除",
+    "close": "關閉",
     "closeAddResourceForm": "關閉表格",
     "closeAddResourceMenu": "關閉資源列表",
     "closeCompanionWindow": "關閉附屬視窗",
     "closeWindow": "關閉視窗",
     "collapseSection": "關閉{{section}}分頁",
     "collapseSidePanel": "關閉邊欄",
+    "collection": "集合",
     "itemList": "標題列表",
+    "continue": "繼續",
     "copy": "複製",
     "currentItem": "目前物件",
     "currentItem_1/1": "目前物件",
     "currentItem_1/2": "左方",
     "currentItem_2/2": "右方",
-    "dark": "黑色主題",
+    "dark": "暗色主題",
+    "digitizedView": "數字視圖",
     "dismiss": "關閉信息",
-    "highlightAllAnnotations": "顯示所有注釋",
+    "highlightAllAnnotations": "高亮所有標註",
+    "displayNoAnnotations": "不高亮",
     "downloadExport": "滙出桌面排版",
     "downloadExportWorkspace": "滙出桌面排版",
     "elastic": "彈性",
     "elasticDescription": "在桌面上自由擺放視窗",
     "emptyResourceList": "資源列表沒有物件",
+    "error": "錯誤",
     "errorDialogConfirm": "確定",
     "errorDialogTitle": "發生錯誤",
     "exitFullScreen": "退出全螢幕",
     "expandSection": "開啟{{section}}分頁",
+    "exportCopied": "工作區配置被複製到你的剪貼板上了",
     "expandSidePanel": "開啟邊欄",
     "fetchManifest": "添加",
     "fullScreen": "全螢幕",
     "gallery": "矩列",
     "hideZoomControls": "隱藏縮放選項",
     "iiif_homepage": "有關此資源",
-    "iiif_manifest": "IIIF",
+    "iiif_manifest": "IIIF清單",
     "iiif_renderings": "其他格式",
     "iiif_seeAlso": "另見",
     "import" : "滙入",
     "importWorkspace": "滙入桌面排版",
     "importWorkspaceHint": "在此貼上Mirador 3排版設定碼",
     "item": "物件: {{label}}",
+    "jsError": "技術細節",
+    "jsStack": "{{ stack }}",
     "language": "語言",
-    "light": "白色主題",
+    "layer_hide": "隱藏圖層",
+    "layer_move": "移動圖層",
+    "layer_opacity": "圖層不透明度",
+    "layer_show": "顯示圖層",
+    "layer_moveToTop": "將圖層移到頂部",
+    "layers": "圖層",
+    "light": "亮色主題",
     "links": "連結",
     "listAllOpenWindows": "切換至視窗",
     "login": "登入",
+    "logout": "登出",
     "manifestError": "無法增添資源:",
     "maximizeWindow": "視窗最大化",
     "minimizeWindow": "視窗最小化",
     "mirador": "Mirador",
     "miradorResources": "Mirador資源",
     "miradorViewer": "Mirador閱覽器",
+    "more": "更多...",
+    "moreResults": "更多結果",
     "mosaic": "馬賽克",
     "mosaicDescription": "在桌面上以格狀方式排列視窗",
     "moveCompanionWindowToBottom": "移至下方",
     "moveCompanionWindowToRight": "移至右方",
+    "multipartCollection": "多卷集合",
     "nextCanvas": "下一頁",
+    "noItemSelected": "沒有物件被選中",
     "numItems": "{{number}} 項物件",
+    "numItems_plural": "{{number}} 項物件",
     "off": "關閉",
-    "openCompanionWindow_annotations": "注釋",
+    "openCompanionWindow_annotations": "標註",
     "openCompanionWindow_attribution": "著作權",
     "openCompanionWindow_canvas": "目錄",
     "openCompanionWindow_info": "資訊",
+    "openCompanionWindow_layers": "圖層",
+    "openCompanionWindow_search": "搜索",
     "openInCompanionWindow": "移至新附屬視窗",
     "openWindows": "現有視窗",
     "pagination": "{{current}} / {{total}}",
@@ -83,34 +111,54 @@
     "previousCanvas": "上一頁",
     "related": "相關資訊",
     "resource": "資源",
+    "retry": "重試",
     "right": "右方",
     "rights": "版權",
+    "scroll": "滾動",
+    "searchInputLabel": "搜索關鍵字",
+    "searchNextResult": "下一個結果",
+    "searchNoResults": "沒有搜索到結果",
+    "searchPreviousResult": "前一個結果",
+    "searchResultsRemaining": "{{numLeft}}剩餘",
+    "searchSubmitAria": "提交搜索",
+    "searchTitle": "搜索",
     "selectWorkspaceMenu": "選擇桌面排版方式",
-    "showingNumAnnotations": "顯示 {{number}} 項注釋",
+    "showingNumAnnotations": "顯示 {{number}} 項標註",
+    "showingNumAnnotations_plural": "顯示 {{number}} 項標註",
+    "showCollection": "顯示集合",
     "showZoomControls": "顯示縮放選項",
     "sidebarPanelsNavigation": "切換邊欄",
     "single": "單項",
     "startHere": "按此開始",
+    "suggestSearch": "搜索本文檔以\"{{ query }}\"",
+    "tableOfContentsList": "目錄",
     "theme": "佈景主題",
-    "thumbnailList": "縮圖列表",
-    "thumbnailNavigation": "縮圖",
-    "thumbnails": "顯示縮圖",
+    "thumbnailList": "縮略圖列表",
+    "thumbnailNavigation": "縮略圖",
+    "thumbnails": "縮略圖",
     "toggleWindowSideBar": "切換邊欄開關",
+    "totalCollections": "{{count}} 集合",
+    "totalCollections_plural": "{{count}} 集合",
+    "totalManifests": "{{count}} 清單",
+    "totalManifests_plural": "{{count}} 清單",
     "tryAgain": "請重試",
     "untitled": "[無標題]",
     "view": "物件排列方式",
+    "viewWorkspaceConfiguration": "查看工作區配置",
     "welcome": "歡迎使用Mirador",
     "window": "視窗: {{label}}",
-    "windowMenu": "視窗選項",
+    "windowMenu": "視窗視圖 & 縮略圖顯示",
     "windowNavigation": "切換視窗",
-    "windowPluginButtons": "插件",
+    "windowPluginButtons": "選項",
+    "windowPluginMenu": "視窗選項",
     "workspace": "桌面",
+    "workspaceNavigation": "工作區導航",
     "workspaceFullScreen": "全螢幕",
     "workspaceMenu": "桌面設定",
     "workspaceOptions": "桌面選項",
     "workspaceSelectionTitle": "選擇桌面排版方式",
     "zoomIn": "放大",
-    "zoomOut": "放小",
+    "zoomOut": "縮小",
     "zoomReset": "重設縮放"
   }
 }
diff --git a/src/state/actions/window.js b/src/state/actions/window.js
index 52d9b4fdb2c10158084a47815d603ff49a1dee5b..c0e63ee91b58fa4c4b7bdfe2f60bbce7bd3c12a8 100644
--- a/src/state/actions/window.js
+++ b/src/state/actions/window.js
@@ -44,10 +44,14 @@ export function addWindow({ companionWindows, manifest, ...options }) {
       ),
     ];
 
-    if (config.window.defaultSideBarPanel || config.window.sideBarPanel) {
+    if (options.sideBarPanel || config.window.defaultSideBarPanel || config.window.sideBarPanel) {
       defaultCompanionWindows.unshift(
         {
-          content: config.window.defaultSideBarPanel || config.window.sideBarPanel,
+          content: options.sideBarPanel
+            || (options.defaultSearchQuery && 'search')
+            || config.window.defaultSideBarPanel
+            || config.window.sideBarPanel,
+
           default: true,
           id: `cw-${uuid()}`,
           position: 'left',
@@ -70,15 +74,16 @@ export function addWindow({ companionWindows, manifest, ...options }) {
       rotation: null,
       selectedAnnotations: {},
       sideBarOpen: config.window.sideBarOpenByDefault !== undefined
-        ? config.window.sideBarOpenByDefault
-        : config.window.sideBarOpen,
-      sideBarPanel: config.window.defaultSideBarPanel || config.window.sideBarPanel,
+        ? config.window.sideBarOpenByDefault || !!options.defaultSearchQuery
+        : config.window.sideBarOpen || !!options.defaultSearchQuery,
+      sideBarPanel: options.sideBarPanel
+        || config.window.defaultSideBarPanel
+        || config.window.sideBarPanel,
       thumbnailNavigationId: cwThumbs,
     };
 
     const elasticLayout = {
-      height: 400,
-      width: 400,
+      ...(config.window.elastic || { height: 400, width: 480 }),
       x: 200 + (Math.floor(numWindows / 10) * 50 + (numWindows * 30) % 300),
       y: 200 + ((numWindows * 50) % 300),
     };
diff --git a/src/state/createPluggableStore.js b/src/state/createPluggableStore.js
new file mode 100644
index 0000000000000000000000000000000000000000..3e3975fe1d169338e08f7947863e75b7e5210b5a
--- /dev/null
+++ b/src/state/createPluggableStore.js
@@ -0,0 +1,31 @@
+import deepmerge from 'deepmerge';
+import createStore from './createStore';
+import { importConfig } from './actions/config';
+import {
+  filterValidPlugins,
+  getConfigFromPlugins,
+  getReducersFromPlugins,
+  getSagasFromPlugins,
+} from '../extend/pluginPreprocessing';
+
+/**
+ * Configure Store
+ */
+function createPluggableStore(config, plugins = []) {
+  const filteredPlugins = filterValidPlugins(plugins);
+
+  const store = createStore(
+    getReducersFromPlugins(filteredPlugins),
+    getSagasFromPlugins(filteredPlugins),
+  );
+
+  store.dispatch(
+    importConfig(
+      deepmerge(getConfigFromPlugins(filteredPlugins), config),
+    ),
+  );
+
+  return store;
+}
+
+export default createPluggableStore;
diff --git a/src/state/createStore.js b/src/state/createStore.js
index 4797c59ebfe99ea942b02576f4e16bee8db49222..717b0b8915cc9dd00df4b0652a56a4d291b80c65 100644
--- a/src/state/createStore.js
+++ b/src/state/createStore.js
@@ -14,7 +14,7 @@ import settings from '../config/settings';
 /**
  * Configure Store
  */
-export default function (pluginReducers, pluginSagas = []) {
+function configureStore(pluginReducers, pluginSagas = []) {
   const miradorReducer = createRootReducer(pluginReducers);
 
   const rootReducer = settings.state.slice
@@ -38,3 +38,5 @@ export default function (pluginReducers, pluginSagas = []) {
 
   return store;
 }
+
+export default configureStore;
diff --git a/src/state/index.js b/src/state/index.js
index b55ed16a04db290df08cf8067f7d9d47d7cb9036..6c5d5191b914e9c460be4240be8ccdce350b4439 100644
--- a/src/state/index.js
+++ b/src/state/index.js
@@ -4,12 +4,10 @@ import * as sagas from './sagas';
 import * as selectors from './selectors';
 import createStore from './createStore';
 
-const exports = {
+export default {
   actions,
   createStore,
   reducers,
   sagas,
   selectors,
 };
-
-export default exports;
diff --git a/src/state/sagas/auth.js b/src/state/sagas/auth.js
index 0ecb94976c362690ffd0360826eb934ce635e7b8..732a482368c8521b09b1970125870d4973580a1d 100644
--- a/src/state/sagas/auth.js
+++ b/src/state/sagas/auth.js
@@ -1,7 +1,7 @@
 import {
   all, call, put, select, takeEvery, delay,
 } from 'redux-saga/effects';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import flatten from 'lodash/flatten';
 import ActionTypes from '../actions/action-types';
 import MiradorCanvas from '../../lib/MiradorCanvas';
diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js
index f0ce4c5371f91f62095e952251dd786e998afec3..41823156bc7b8ac6b423ec9dafe4e8efdb019e53 100644
--- a/src/state/sagas/iiif.js
+++ b/src/state/sagas/iiif.js
@@ -2,7 +2,7 @@ import {
   all, call, put, select, takeEvery,
 } from 'redux-saga/effects';
 import fetch from 'isomorphic-unfetch';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import normalizeUrl from 'normalize-url';
 import ActionTypes from '../actions/action-types';
 import {
diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js
index 8ad878a35124110d53c32ac10cb1e03fe1b87ff3..bc63cdcbb9a072fbdb5f4dd4dd23890d92974823 100644
--- a/src/state/sagas/windows.js
+++ b/src/state/sagas/windows.js
@@ -27,6 +27,7 @@ import {
   getElasticLayout,
   getCanvases,
   selectInfoResponses,
+  getWindowConfig,
 } from '../selectors';
 import { fetchManifests } from './iiif';
 
@@ -202,6 +203,10 @@ export function* updateVisibleCanvases({ windowId }) {
 
 /** @private */
 export function* setCanvasOfFirstSearchResult({ companionWindowId, windowId }) {
+  const { switchCanvasOnSearch } = yield select(getWindowConfig, { windowId });
+  if (!switchCanvasOnSearch) {
+    return;
+  }
   const selectedIds = yield select(getSelectedContentSearchAnnotationIds, {
     companionWindowId, windowId,
   });
diff --git a/src/state/selectors/auth.js b/src/state/selectors/auth.js
index 07ce1253686872bdf505c5babda8b7c849098d62..7c290bfca47b82095b693d23e3d518f55db2803b 100644
--- a/src/state/selectors/auth.js
+++ b/src/state/selectors/auth.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import flatten from 'lodash/flatten';
 import MiradorCanvas from '../../lib/MiradorCanvas';
 import { miradorSlice } from './utils';
diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js
index 63a726680aa8929779c2888c5cc1635b424bdbb5..44abe0435266fe581b624657d5207af2e5076a33 100644
--- a/src/state/selectors/manifests.js
+++ b/src/state/selectors/manifests.js
@@ -1,8 +1,8 @@
 import { createSelector } from 'reselect';
 import createCachedSelector from 're-reselect';
-import { PropertyValue } from 'manifesto.js/dist-esmodule/PropertyValue';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { PropertyValue, Utils } from 'manifesto.js';
 import getThumbnail from '../../lib/ThumbnailFactory';
+import asArray from '../../lib/asArray';
 import { getCompanionWindow } from './companionWindows';
 import { getManifest } from './getters';
 import { getConfig } from './config';
@@ -109,16 +109,6 @@ export const getManifestProvider = createSelector(
     && PropertyValue.parse(provider[0].label, locale).getValue(),
 );
 
-/**
- */
-function asArray(value) {
-  if (!Array.isArray(value)) {
-    return [value];
-  }
-
-  return value;
-}
-
 /**
 * Return the IIIF v3 homepage of a manifest or null
 * @param {object} state
@@ -234,10 +224,13 @@ export const getRights = createSelector(
 */
 export function getManifestThumbnail(state, props) {
   const manifest = getManifestoInstance(state, props);
+  const { thumbnails = {} } = getConfig(state);
 
   if (!manifest) return undefined;
 
-  const thumbnail = getThumbnail(manifest, { maxHeight: 80, maxWidth: 120 });
+  const thumbnail = getThumbnail(manifest, {
+    maxHeight: 80, maxWidth: 120, preferredFormats: thumbnails.preferredFormats,
+  });
 
   return thumbnail && thumbnail.url;
 }
diff --git a/src/state/selectors/ranges.js b/src/state/selectors/ranges.js
index 0a40388874a82176fc62b62320bf212d66510679..a2e312955850fd488421b479a8b6d4f4cecda7ec 100644
--- a/src/state/selectors/ranges.js
+++ b/src/state/selectors/ranges.js
@@ -1,7 +1,7 @@
 import { createSelector } from 'reselect';
 import union from 'lodash/union';
 import without from 'lodash/without';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import { Utils } from 'manifesto.js';
 import { getVisibleCanvasIds } from './canvases';
 import { getCompanionWindow } from './companionWindows';
 import { getSequenceTreeStructure } from './sequences';
diff --git a/src/state/selectors/searches.js b/src/state/selectors/searches.js
index f679b1b59cf08a12610193dddede306ff1e9ccc0..3e541084e325e3ca41cd962cf119dc91cf15af3a 100644
--- a/src/state/selectors/searches.js
+++ b/src/state/selectors/searches.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import { PropertyValue } from 'manifesto.js/dist-esmodule/PropertyValue';
+import { PropertyValue } from 'manifesto.js';
 import flatten from 'lodash/flatten';
 import AnnotationList from '../../lib/AnnotationList';
 import { getCanvas, getCanvases } from './canvases';
@@ -57,6 +57,22 @@ export const getSearchIsFetching = createSelector(
   results => results.some(result => result.isFetching),
 );
 
+export const getSearchNumTotal = createSelector(
+  [
+    getSearchForCompanionWindow,
+  ],
+  (results) => {
+    if (!results || !results.data) return undefined;
+
+    const resultWithWithin = Object.values(results.data).find(result => (
+      !result.isFetching
+        && result.json
+        && result.json.within
+    ));
+    return resultWithWithin?.json?.within?.total;
+  },
+);
+
 export const getNextSearchId = createSelector(
   [
     getSearchForCompanionWindow,
diff --git a/src/state/selectors/sequences.js b/src/state/selectors/sequences.js
index 6e90dc08b87958722518462470f34626e2321d38..c8014a70c0d3860dba8aaa703c9c13e880937bc8 100644
--- a/src/state/selectors/sequences.js
+++ b/src/state/selectors/sequences.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import { TreeNode } from 'manifesto.js/dist-esmodule/TreeNode';
+import { TreeNode } from 'manifesto.js';
 import {
   getManifestoInstance,
 } from './manifests';
diff --git a/webpack.config.js b/webpack.config.js
index 292c2e3b62e4cf3933bd0a59d2bee331e9c3b649..3f78b6cad3e2a3339c9801046e1b21ef5bb89b0a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,8 +1,8 @@
 const path = require('path');
+const fs = require('fs');
 const webpack = require('webpack');
 const TerserPlugin = require('terser-webpack-plugin');
 const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
-const paths = require('./config/paths');
 
 /** */
 const baseConfig = mode => ({
@@ -10,7 +10,7 @@ const baseConfig = mode => ({
   module: {
     rules: [
       {
-        include: paths.appPath, // CRL
+        include: path.resolve(fs.realpathSync(process.cwd()), '.'), // CRL
         loader: require.resolve('babel-loader'),
         options: {
           // Save disk space when time isn't as important