diff --git a/.gitignore b/.gitignore
index 362df57e1..83b754c04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ _ide_helper.php
 /storage/debugbar
 .phpstorm.meta.php
 yarn.lock
+/bin
\ No newline at end of file
diff --git a/app/Chapter.php b/app/Chapter.php
index cc5518b7a..dc23f5ebd 100644
--- a/app/Chapter.php
+++ b/app/Chapter.php
@@ -5,6 +5,8 @@ class Chapter extends Entity
 {
     protected $fillable = ['name', 'description', 'priority', 'book_id'];
 
+    protected $with = ['book'];
+
     /**
      * Get the book this chapter is within.
      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@@ -16,11 +18,12 @@ class Chapter extends Entity
 
     /**
      * Get the pages that this chapter contains.
+     * @param string $dir
      * @return mixed
      */
-    public function pages()
+    public function pages($dir = 'ASC')
     {
-        return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
+        return $this->hasMany(Page::class)->orderBy('priority', $dir);
     }
 
     /**
diff --git a/app/Entity.php b/app/Entity.php
index 186059f00..e8deddf0a 100644
--- a/app/Entity.php
+++ b/app/Entity.php
@@ -4,6 +4,8 @@
 class Entity extends Ownable
 {
 
+    protected $fieldsToSearch = ['name', 'description'];
+
     /**
      * Compares this entity to another given entity.
      * Matches by comparing class and id.
@@ -157,7 +159,7 @@ class Entity extends Ownable
      * @param string[] array $wheres
      * @return mixed
      */
-    public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
+    public function fullTextSearchQuery($terms, $wheres = [])
     {
         $exactTerms = [];
         $fuzzyTerms = [];
@@ -181,16 +183,16 @@ class Entity extends Ownable
         // Perform fulltext search if relevant terms exist.
         if ($isFuzzy) {
             $termString = implode(' ', $fuzzyTerms);
-            $fields = implode(',', $fieldsToSearch);
+            $fields = implode(',', $this->fieldsToSearch);
             $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
             $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
         }
 
         // Ensure at least one exact term matches if in search
         if (count($exactTerms) > 0) {
-            $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
+            $search = $search->where(function ($query) use ($exactTerms) {
                 foreach ($exactTerms as $exactTerm) {
-                    foreach ($fieldsToSearch as $field) {
+                    foreach ($this->fieldsToSearch as $field) {
                         $query->orWhere($field, 'like', $exactTerm);
                     }
                 }
diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php
index 62be0b852..3c325d0fe 100644
--- a/app/Http/Controllers/AttachmentController.php
+++ b/app/Http/Controllers/AttachmentController.php
@@ -2,7 +2,7 @@
 
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Attachment;
-use BookStack\Repos\PageRepo;
+use BookStack\Repos\EntityRepo;
 use BookStack\Services\AttachmentService;
 use Illuminate\Http\Request;
 
@@ -10,19 +10,19 @@ class AttachmentController extends Controller
 {
     protected $attachmentService;
     protected $attachment;
-    protected $pageRepo;
+    protected $entityRepo;
 
     /**
      * AttachmentController constructor.
      * @param AttachmentService $attachmentService
      * @param Attachment $attachment
-     * @param PageRepo $pageRepo
+     * @param EntityRepo $entityRepo
      */
-    public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
+    public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
     {
         $this->attachmentService = $attachmentService;
         $this->attachment = $attachment;
-        $this->pageRepo = $pageRepo;
+        $this->entityRepo = $entityRepo;
         parent::__construct();
     }
 
@@ -40,7 +40,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -70,14 +70,14 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('attachment-create', $attachment);
         
         if (intval($pageId) !== intval($attachment->uploaded_to)) {
-            return $this->jsonError('Page mismatch during attached file update');
+            return $this->jsonError(trans('errors.attachment_page_mismatch'));
         }
 
         $uploadedFile = $request->file('file');
@@ -106,18 +106,18 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('attachment-create', $attachment);
 
         if (intval($pageId) !== intval($attachment->uploaded_to)) {
-            return $this->jsonError('Page mismatch during attachment update');
+            return $this->jsonError(trans('errors.attachment_page_mismatch'));
         }
 
         $attachment = $this->attachmentService->updateFile($attachment, $request->all());
-        return $attachment;
+        return response()->json($attachment);
     }
 
     /**
@@ -134,7 +134,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -153,7 +153,7 @@ class AttachmentController extends Controller
      */
     public function listForPage($pageId)
     {
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
         $this->checkOwnablePermission('page-view', $page);
         return response()->json($page->attachments);
     }
@@ -170,12 +170,12 @@ class AttachmentController extends Controller
             'files' => 'required|array',
             'files.*.id' => 'required|integer',
         ]);
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->entityRepo->getById('page', $pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         $attachments = $request->get('files');
         $this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
-        return response()->json(['message' => 'Attachment order updated']);
+        return response()->json(['message' => trans('entities.attachments_order_updated')]);
     }
 
     /**
@@ -186,7 +186,7 @@ class AttachmentController extends Controller
     public function get($attachmentId)
     {
         $attachment = $this->attachment->findOrFail($attachmentId);
-        $page = $this->pageRepo->getById($attachment->uploaded_to);
+        $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
         $this->checkOwnablePermission('page-view', $page);
 
         if ($attachment->external) {
@@ -210,6 +210,6 @@ class AttachmentController extends Controller
         $attachment = $this->attachment->findOrFail($attachmentId);
         $this->checkOwnablePermission('attachment-delete', $attachment);
         $this->attachmentService->deleteFile($attachment);
-        return response()->json(['message' => 'Attachment deleted']);
+        return response()->json(['message' => trans('entities.attachments_deleted')]);
     }
 }
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
index 45e40e6fe..d1fbddc50 100644
--- a/app/Http/Controllers/Auth/ForgotPasswordController.php
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -52,7 +52,7 @@ class ForgotPasswordController extends Controller
         );
 
         if ($response === Password::RESET_LINK_SENT) {
-            $message = 'A password reset link has been sent to ' . $request->get('email') . '.';
+            $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
             session()->flash('success', $message);
             return back()->with('status', trans($response));
         }
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index c9d6a5496..e7eeb9bc1 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -87,7 +87,7 @@ class LoginController extends Controller
             // Check for users with same email already
             $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
             if ($alreadyUser) {
-                throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
+                throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
             }
 
             $user->save();
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index d9bb500b4..8b0ef309a 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -3,6 +3,7 @@
 namespace BookStack\Http\Controllers\Auth;
 
 use BookStack\Exceptions\ConfirmationEmailException;
+use BookStack\Exceptions\SocialSignInException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Repos\UserRepo;
 use BookStack\Services\EmailConfirmationService;
@@ -82,7 +83,7 @@ class RegisterController extends Controller
     protected function checkRegistrationAllowed()
     {
         if (!setting('registration-enabled')) {
-            throw new UserRegistrationException('Registrations are currently disabled.', '/login');
+            throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
         }
     }
 
@@ -147,7 +148,7 @@ class RegisterController extends Controller
             $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
             $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
             if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
-                throw new UserRegistrationException('That email domain does not have access to this application', '/register');
+                throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
             }
         }
 
@@ -169,7 +170,7 @@ class RegisterController extends Controller
         }
 
         auth()->login($newUser);
-        session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
+        session()->flash('success', trans('auth.register_success'));
         return redirect($this->redirectPath());
     }
 
@@ -262,7 +263,7 @@ class RegisterController extends Controller
                 return $this->socialRegisterCallback($socialDriver);
             }
         } else {
-            throw new SocialSignInException('No action defined', '/login');
+            throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
         }
         return redirect()->back();
     }
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
index bd64793f9..eb678503d 100644
--- a/app/Http/Controllers/Auth/ResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -41,7 +41,7 @@ class ResetPasswordController extends Controller
      */
     protected function sendResetResponse($response)
     {
-        $message = 'Your password has been successfully reset.';
+        $message = trans('auth.reset_password_success');
         session()->flash('success', $message);
         return redirect($this->redirectPath())
             ->with('status', trans($response));
diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php
index 8ada59433..57ac486d5 100644
--- a/app/Http/Controllers/BookController.php
+++ b/app/Http/Controllers/BookController.php
@@ -1,34 +1,26 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
+use BookStack\Repos\EntityRepo;
 use BookStack\Repos\UserRepo;
 use Illuminate\Http\Request;
-use BookStack\Http\Requests;
-use BookStack\Repos\BookRepo;
-use BookStack\Repos\ChapterRepo;
-use BookStack\Repos\PageRepo;
+use Illuminate\Http\Response;
 use Views;
 
 class BookController extends Controller
 {
 
-    protected $bookRepo;
-    protected $pageRepo;
-    protected $chapterRepo;
+    protected $entityRepo;
     protected $userRepo;
 
     /**
      * BookController constructor.
-     * @param BookRepo $bookRepo
-     * @param PageRepo $pageRepo
-     * @param ChapterRepo $chapterRepo
+     * @param EntityRepo $entityRepo
      * @param UserRepo $userRepo
      */
-    public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
+    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
     {
-        $this->bookRepo = $bookRepo;
-        $this->pageRepo = $pageRepo;
-        $this->chapterRepo = $chapterRepo;
+        $this->entityRepo = $entityRepo;
         $this->userRepo = $userRepo;
         parent::__construct();
     }
@@ -39,9 +31,9 @@ class BookController extends Controller
      */
     public function index()
     {
-        $books = $this->bookRepo->getAllPaginated(10);
-        $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false;
-        $popular = $this->bookRepo->getPopular(4, 0);
+        $books = $this->entityRepo->getAllPaginated('book', 10);
+        $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
+        $popular = $this->entityRepo->getPopular('book', 4, 0);
         $this->setPageTitle('Books');
         return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
     }
@@ -53,7 +45,7 @@ class BookController extends Controller
     public function create()
     {
         $this->checkPermission('book-create-all');
-        $this->setPageTitle('Create New Book');
+        $this->setPageTitle(trans('entities.books_create'));
         return view('books/create');
     }
 
@@ -70,7 +62,7 @@ class BookController extends Controller
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000'
         ]);
-        $book = $this->bookRepo->createFromInput($request->all());
+        $book = $this->entityRepo->createFromInput('book', $request->all());
         Activity::add($book, 'book_create', $book->id);
         return redirect($book->getUrl());
     }
@@ -82,9 +74,9 @@ class BookController extends Controller
      */
     public function show($slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->entityRepo->getBySlug('book', $slug);
         $this->checkOwnablePermission('book-view', $book);
-        $bookChildren = $this->bookRepo->getChildren($book);
+        $bookChildren = $this->entityRepo->getBookChildren($book);
         Views::add($book);
         $this->setPageTitle($book->getShortName());
         return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
@@ -97,9 +89,9 @@ class BookController extends Controller
      */
     public function edit($slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->entityRepo->getBySlug('book', $slug);
         $this->checkOwnablePermission('book-update', $book);
-        $this->setPageTitle('Edit Book ' . $book->getShortName());
+        $this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
         return view('books/edit', ['book' => $book, 'current' => $book]);
     }
 
@@ -111,13 +103,13 @@ class BookController extends Controller
      */
     public function update(Request $request, $slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->entityRepo->getBySlug('book', $slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000'
         ]);
-        $book = $this->bookRepo->updateFromInput($book, $request->all());
+        $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
         Activity::add($book, 'book_update', $book->id);
         return redirect($book->getUrl());
     }
@@ -129,9 +121,9 @@ class BookController extends Controller
      */
     public function showDelete($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
-        $this->setPageTitle('Delete Book ' . $book->getShortName());
+        $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
         return view('books/delete', ['book' => $book, 'current' => $book]);
     }
 
@@ -142,11 +134,11 @@ class BookController extends Controller
      */
     public function sort($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('book-update', $book);
-        $bookChildren = $this->bookRepo->getChildren($book, true);
-        $books = $this->bookRepo->getAll(false);
-        $this->setPageTitle('Sort Book ' . $book->getShortName());
+        $bookChildren = $this->entityRepo->getBookChildren($book, true);
+        $books = $this->entityRepo->getAll('book', false);
+        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
         return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
     }
 
@@ -158,8 +150,8 @@ class BookController extends Controller
      */
     public function getSortItem($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $bookChildren = $this->bookRepo->getChildren($book);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $bookChildren = $this->entityRepo->getBookChildren($book);
         return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
     }
 
@@ -171,7 +163,7 @@ class BookController extends Controller
      */
     public function saveSort($bookSlug, Request $request)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('book-update', $book);
 
         // Return if no map sent
@@ -190,13 +182,13 @@ class BookController extends Controller
             $priority = $bookChild->sort;
             $id = intval($bookChild->id);
             $isPage = $bookChild->type == 'page';
-            $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
+            $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
             $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
-            $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
+            $model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
 
             // Update models only if there's a change in parent chain or ordering.
             if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
-                $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
+                $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
                 $model->priority = $priority;
                 if ($isPage) $model->chapter_id = $chapterId;
                 $model->save();
@@ -211,12 +203,12 @@ class BookController extends Controller
 
         // Add activity for books
         foreach ($sortedBooks as $bookId) {
-            $updatedBook = $this->bookRepo->getById($bookId);
+            $updatedBook = $this->entityRepo->getById('book', $bookId);
             Activity::add($updatedBook, 'book_sort', $updatedBook->id);
         }
 
         // Update permissions on changed models
-        $this->bookRepo->buildJointPermissions($updatedModels);
+        $this->entityRepo->buildJointPermissions($updatedModels);
 
         return redirect($book->getUrl());
     }
@@ -228,11 +220,10 @@ class BookController extends Controller
      */
     public function destroy($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
         Activity::addMessage('book_delete', 0, $book->name);
-        Activity::removeEntity($book);
-        $this->bookRepo->destroy($book);
+        $this->entityRepo->destroyBook($book);
         return redirect('/books');
     }
 
@@ -243,7 +234,7 @@ class BookController extends Controller
      */
     public function showRestrict($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
         $roles = $this->userRepo->getRestrictableRoles();
         return view('books/restrictions', [
@@ -261,10 +252,10 @@ class BookController extends Controller
      */
     public function restrict($bookSlug, Request $request)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
-        $this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
-        session()->flash('success', 'Book Restrictions Updated');
+        $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
+        session()->flash('success', trans('entities.books_permissions_updated'));
         return redirect($book->getUrl());
     }
 }
diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php
index a3fb600fd..1760ee5c6 100644
--- a/app/Http/Controllers/ChapterController.php
+++ b/app/Http/Controllers/ChapterController.php
@@ -1,30 +1,26 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
+use BookStack\Repos\EntityRepo;
 use BookStack\Repos\UserRepo;
 use Illuminate\Http\Request;
-use BookStack\Http\Requests;
-use BookStack\Repos\BookRepo;
-use BookStack\Repos\ChapterRepo;
+use Illuminate\Http\Response;
 use Views;
 
 class ChapterController extends Controller
 {
 
-    protected $bookRepo;
-    protected $chapterRepo;
     protected $userRepo;
+    protected $entityRepo;
 
     /**
      * ChapterController constructor.
-     * @param BookRepo $bookRepo
-     * @param ChapterRepo $chapterRepo
+     * @param EntityRepo $entityRepo
      * @param UserRepo $userRepo
      */
-    public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
+    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
     {
-        $this->bookRepo = $bookRepo;
-        $this->chapterRepo = $chapterRepo;
+        $this->entityRepo = $entityRepo;
         $this->userRepo = $userRepo;
         parent::__construct();
     }
@@ -36,9 +32,9 @@ class ChapterController extends Controller
      */
     public function create($bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('chapter-create', $book);
-        $this->setPageTitle('Create New Chapter');
+        $this->setPageTitle(trans('entities.chapters_create'));
         return view('chapters/create', ['book' => $book, 'current' => $book]);
     }
 
@@ -54,12 +50,12 @@ class ChapterController extends Controller
             'name' => 'required|string|max:255'
         ]);
 
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('chapter-create', $book);
 
         $input = $request->all();
-        $input['priority'] = $this->bookRepo->getNewPriority($book);
-        $chapter = $this->chapterRepo->createFromInput($input, $book);
+        $input['priority'] = $this->entityRepo->getNewBookPriority($book);
+        $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
         Activity::add($chapter, 'chapter_create', $book->id);
         return redirect($chapter->getUrl());
     }
@@ -72,15 +68,14 @@ class ChapterController extends Controller
      */
     public function show($bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
-        $sidebarTree = $this->bookRepo->getChildren($book);
+        $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
         Views::add($chapter);
         $this->setPageTitle($chapter->getShortName());
-        $pages = $this->chapterRepo->getChildren($chapter);
+        $pages = $this->entityRepo->getChapterChildren($chapter);
         return view('chapters/show', [
-            'book' => $book,
+            'book' => $chapter->book,
             'chapter' => $chapter,
             'current' => $chapter,
             'sidebarTree' => $sidebarTree,
@@ -96,11 +91,10 @@ class ChapterController extends Controller
      */
     public function edit($bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
-        $this->setPageTitle('Edit Chapter' . $chapter->getShortName());
-        return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
+        $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
+        return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
@@ -112,16 +106,15 @@ class ChapterController extends Controller
      */
     public function update(Request $request, $bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         if ($chapter->name !== $request->get('name')) {
-            $chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id);
+            $chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
         }
         $chapter->fill($request->all());
         $chapter->updated_by = user()->id;
         $chapter->save();
-        Activity::add($chapter, 'chapter_update', $book->id);
+        Activity::add($chapter, 'chapter_update', $chapter->book->id);
         return redirect($chapter->getUrl());
     }
 
@@ -133,11 +126,10 @@ class ChapterController extends Controller
      */
     public function showDelete($bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
-        $this->setPageTitle('Delete Chapter' . $chapter->getShortName());
-        return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
+        $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
+        return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
@@ -148,11 +140,11 @@ class ChapterController extends Controller
      */
     public function destroy($bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $book = $chapter->book;
         $this->checkOwnablePermission('chapter-delete', $chapter);
         Activity::addMessage('chapter_delete', $book->id, $chapter->name);
-        $this->chapterRepo->destroy($chapter);
+        $this->entityRepo->destroyChapter($chapter);
         return redirect($book->getUrl());
     }
 
@@ -164,12 +156,12 @@ class ChapterController extends Controller
      * @throws \BookStack\Exceptions\NotFoundException
      */
     public function showMove($bookSlug, $chapterSlug) {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
         return view('chapters/move', [
             'chapter' => $chapter,
-            'book' => $book
+            'book' => $chapter->book
         ]);
     }
 
@@ -182,8 +174,7 @@ class ChapterController extends Controller
      * @throws \BookStack\Exceptions\NotFoundException
      */
     public function move($bookSlug, $chapterSlug, Request $request) {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $entitySelection = $request->get('entity_selection', null);
@@ -198,17 +189,17 @@ class ChapterController extends Controller
         $parent = false;
 
         if ($entityType == 'book') {
-            $parent = $this->bookRepo->getById($entityId);
+            $parent = $this->entityRepo->getById('book', $entityId);
         }
 
         if ($parent === false || $parent === null) {
-            session()->flash('The selected Book was not found');
+            session()->flash('error', trans('errors.selected_book_not_found'));
             return redirect()->back();
         }
 
-        $this->chapterRepo->changeBook($parent->id, $chapter, true);
+        $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
         Activity::add($chapter, 'chapter_move', $chapter->book->id);
-        session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
+        session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
 
         return redirect($chapter->getUrl());
     }
@@ -221,8 +212,7 @@ class ChapterController extends Controller
      */
     public function showRestrict($bookSlug, $chapterSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
         $roles = $this->userRepo->getRestrictableRoles();
         return view('chapters/restrictions', [
@@ -240,11 +230,10 @@ class ChapterController extends Controller
      */
     public function restrict($bookSlug, $chapterSlug, Request $request)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
-        $this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
-        session()->flash('success', 'Chapter Restrictions Updated');
+        $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
+        session()->flash('success', trans('entities.chapters_permissions_success'));
         return redirect($chapter->getUrl());
     }
 }
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 2fc64b236..f4706a5c4 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
 use Activity;
 use BookStack\Repos\EntityRepo;
 use BookStack\Http\Requests;
+use Illuminate\Http\Response;
 use Views;
 
 class HomeController extends Controller
@@ -31,9 +32,9 @@ class HomeController extends Controller
         $activity = Activity::latest(10);
         $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
         $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
-        $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
-        $recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
-        $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
+        $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
+        $recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
+        $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
         return view('home', [
             'activity' => $activity,
             'recents' => $recents,
@@ -43,4 +44,39 @@ class HomeController extends Controller
         ]);
     }
 
+    /**
+     * Get a js representation of the current translations
+     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
+     */
+    public function getTranslations() {
+        $locale = trans()->getLocale();
+        $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
+        if (cache()->has($cacheKey) && config('app.env') !== 'development') {
+            $resp = cache($cacheKey);
+        } else {
+            $translations = [
+                // Get only translations which might be used in JS
+                'common' => trans('common'),
+                'components' => trans('components'),
+                'entities' => trans('entities'),
+                'errors' => trans('errors')
+            ];
+            if ($locale !== 'en') {
+                $enTrans = [
+                    'common' => trans('common', [], null, 'en'),
+                    'components' => trans('components', [], null, 'en'),
+                    'entities' => trans('entities', [], null, 'en'),
+                    'errors' => trans('errors', [], null, 'en')
+                ];
+                $translations = array_replace_recursive($enTrans, $translations);
+            }
+            $resp = 'window.translations = ' . json_encode($translations);
+            cache()->put($cacheKey, $resp, 120);
+        }
+
+        return response($resp, 200, [
+            'Content-Type' => 'application/javascript'
+        ]);
+    }
+
 }
diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php
index 621c23e85..77c320e07 100644
--- a/app/Http/Controllers/ImageController.php
+++ b/app/Http/Controllers/ImageController.php
@@ -1,6 +1,7 @@
 <?php namespace BookStack\Http\Controllers;
 
 use BookStack\Exceptions\ImageUploadException;
+use BookStack\Repos\EntityRepo;
 use BookStack\Repos\ImageRepo;
 use Illuminate\Filesystem\Filesystem as File;
 use Illuminate\Http\Request;
@@ -73,6 +74,7 @@ class ImageController extends Controller
      * @param $filter
      * @param int $page
      * @param Request $request
+     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
      */
     public function getGalleryFiltered($filter, $page = 0, Request $request)
     {
@@ -149,12 +151,12 @@ class ImageController extends Controller
 
     /**
      * Deletes an image and all thumbnail/image files
-     * @param PageRepo $pageRepo
+     * @param EntityRepo $entityRepo
      * @param Request $request
      * @param int $id
      * @return \Illuminate\Http\JsonResponse
      */
-    public function destroy(PageRepo $pageRepo, Request $request, $id)
+    public function destroy(EntityRepo $entityRepo, Request $request, $id)
     {
         $image = $this->imageRepo->getById($id);
         $this->checkOwnablePermission('image-delete', $image);
@@ -162,14 +164,14 @@ class ImageController extends Controller
         // Check if this image is used on any pages
         $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
         if (!$isForced) {
-            $pageSearch = $pageRepo->searchForImage($image->url);
+            $pageSearch = $entityRepo->searchForImage($image->url);
             if ($pageSearch !== false) {
                 return response()->json($pageSearch, 400);
             }
         }
 
         $this->imageRepo->destroyImage($image);
-        return response()->json('Image Deleted');
+        return response()->json(trans('components.images_deleted'));
     }
 
 
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index c2d8e257c..6ed9fc30c 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -2,40 +2,31 @@
 
 use Activity;
 use BookStack\Exceptions\NotFoundException;
+use BookStack\Repos\EntityRepo;
 use BookStack\Repos\UserRepo;
 use BookStack\Services\ExportService;
 use Carbon\Carbon;
 use Illuminate\Http\Request;
-use BookStack\Http\Requests;
-use BookStack\Repos\BookRepo;
-use BookStack\Repos\ChapterRepo;
-use BookStack\Repos\PageRepo;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Illuminate\Http\Response;
 use Views;
 use GatherContent\Htmldiff\Htmldiff;
 
 class PageController extends Controller
 {
 
-    protected $pageRepo;
-    protected $bookRepo;
-    protected $chapterRepo;
+    protected $entityRepo;
     protected $exportService;
     protected $userRepo;
 
     /**
      * PageController constructor.
-     * @param PageRepo $pageRepo
-     * @param BookRepo $bookRepo
-     * @param ChapterRepo $chapterRepo
+     * @param EntityRepo $entityRepo
      * @param ExportService $exportService
      * @param UserRepo $userRepo
      */
-    public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
+    public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
     {
-        $this->pageRepo = $pageRepo;
-        $this->bookRepo = $bookRepo;
-        $this->chapterRepo = $chapterRepo;
+        $this->entityRepo = $entityRepo;
         $this->exportService = $exportService;
         $this->userRepo = $userRepo;
         parent::__construct();
@@ -50,19 +41,19 @@ class PageController extends Controller
      */
     public function create($bookSlug, $chapterSlug = null)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
         $parent = $chapter ? $chapter : $book;
         $this->checkOwnablePermission('page-create', $parent);
 
         // Redirect to draft edit screen if signed in
         if ($this->signedIn) {
-            $draft = $this->pageRepo->getDraftPage($book, $chapter);
+            $draft = $this->entityRepo->getDraftPage($book, $chapter);
             return redirect($draft->getUrl());
         }
 
         // Otherwise show edit view
-        $this->setPageTitle('Create New Page');
+        $this->setPageTitle(trans('entities.pages_new'));
         return view('pages/guest-create', ['parent' => $parent]);
     }
 
@@ -80,13 +71,13 @@ class PageController extends Controller
             'name' => 'required|string|max:255'
         ]);
 
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
         $parent = $chapter ? $chapter : $book;
         $this->checkOwnablePermission('page-create', $parent);
 
-        $page = $this->pageRepo->getDraftPage($book, $chapter);
-        $this->pageRepo->publishDraft($page, [
+        $page = $this->entityRepo->getDraftPage($book, $chapter);
+        $this->entityRepo->publishPageDraft($page, [
             'name' => $request->get('name'),
             'html' => ''
         ]);
@@ -101,15 +92,14 @@ class PageController extends Controller
      */
     public function editDraft($bookSlug, $pageId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $draft = $this->pageRepo->getById($pageId, true);
-        $this->checkOwnablePermission('page-create', $book);
-        $this->setPageTitle('Edit Page Draft');
+        $draft = $this->entityRepo->getById('page', $pageId, true);
+        $this->checkOwnablePermission('page-create', $draft->book);
+        $this->setPageTitle(trans('entities.pages_edit_draft'));
 
         $draftsEnabled = $this->signedIn;
         return view('pages/edit', [
             'page' => $draft,
-            'book' => $book,
+            'book' => $draft->book,
             'isDraft' => true,
             'draftsEnabled' => $draftsEnabled
         ]);
@@ -119,6 +109,7 @@ class PageController extends Controller
      * Store a new page by changing a draft into a page.
      * @param  Request $request
      * @param  string $bookSlug
+     * @param  int $pageId
      * @return Response
      */
     public function store(Request $request, $bookSlug, $pageId)
@@ -128,21 +119,21 @@ class PageController extends Controller
         ]);
 
         $input = $request->all();
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->entityRepo->getBySlug('book', $bookSlug);
 
-        $draftPage = $this->pageRepo->getById($pageId, true);
+        $draftPage = $this->entityRepo->getById('page', $pageId, true);
 
         $chapterId = intval($draftPage->chapter_id);
-        $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
+        $parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
         $this->checkOwnablePermission('page-create', $parent);
 
         if ($parent->isA('chapter')) {
-            $input['priority'] = $this->chapterRepo->getNewPriority($parent);
+            $input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
         } else {
-            $input['priority'] = $this->bookRepo->getNewPriority($parent);
+            $input['priority'] = $this->entityRepo->getNewBookPriority($parent);
         }
 
-        $page = $this->pageRepo->publishDraft($draftPage, $input);
+        $page = $this->entityRepo->publishPageDraft($draftPage, $input);
 
         Activity::add($page, 'page_create', $book->id);
         return redirect($page->getUrl());
@@ -150,32 +141,29 @@ class PageController extends Controller
 
     /**
      * Display the specified page.
-     * If the page is not found via the slug the
-     * revisions are searched for a match.
+     * If the page is not found via the slug the revisions are searched for a match.
      * @param string $bookSlug
      * @param string $pageSlug
      * @return Response
      */
     public function show($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-
         try {
-            $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+            $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         } catch (NotFoundException $e) {
-            $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
+            $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
             if ($page === null) abort(404);
             return redirect($page->getUrl());
         }
 
         $this->checkOwnablePermission('page-view', $page);
 
-        $sidebarTree = $this->bookRepo->getChildren($book);
-        $pageNav = $this->pageRepo->getPageNav($page);
+        $sidebarTree = $this->entityRepo->getBookChildren($page->book);
+        $pageNav = $this->entityRepo->getPageNav($page);
         
         Views::add($page);
         $this->setPageTitle($page->getShortName());
-        return view('pages/show', ['page' => $page, 'book' => $book,
+        return view('pages/show', ['page' => $page, 'book' => $page->book,
                                    'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
     }
 
@@ -186,7 +174,7 @@ class PageController extends Controller
      */
     public function getPageAjax($pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->entityRepo->getById('page', $pageId);
         return response()->json($page);
     }
 
@@ -198,26 +186,25 @@ class PageController extends Controller
      */
     public function edit($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->setPageTitle('Editing Page ' . $page->getShortName());
+        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
         $page->isDraft = false;
 
         // Check for active editing
         $warnings = [];
-        if ($this->pageRepo->isPageEditingActive($page, 60)) {
-            $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
+        if ($this->entityRepo->isPageEditingActive($page, 60)) {
+            $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
         }
 
         // Check for a current draft version for this user
-        if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
-            $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
+        if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
+            $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
             $page->name = $draft->name;
             $page->html = $draft->html;
             $page->markdown = $draft->markdown;
             $page->isDraft = true;
-            $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
+            $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
         }
 
         if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
@@ -225,7 +212,7 @@ class PageController extends Controller
         $draftsEnabled = $this->signedIn;
         return view('pages/edit', [
             'page' => $page,
-            'book' => $book,
+            'book' => $page->book,
             'current' => $page,
             'draftsEnabled' => $draftsEnabled
         ]);
@@ -243,11 +230,10 @@ class PageController extends Controller
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->pageRepo->updatePage($page, $book->id, $request->all());
-        Activity::add($page, 'page_update', $book->id);
+        $this->entityRepo->updatePage($page, $page->book->id, $request->all());
+        Activity::add($page, 'page_update', $page->book->id);
         return redirect($page->getUrl());
     }
 
@@ -259,27 +245,23 @@ class PageController extends Controller
      */
     public function saveDraft(Request $request, $pageId)
     {
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
         $this->checkOwnablePermission('page-update', $page);
 
         if (!$this->signedIn) {
             return response()->json([
                 'status' => 'error',
-                'message' => 'Guests cannot save drafts',
+                'message' => trans('errors.guests_cannot_save_drafts'),
             ], 500);
         }
 
-        if ($page->draft) {
-            $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
-        } else {
-            $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
-        }
+        $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
 
         $updateTime = $draft->updated_at->timestamp;
         $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
         return response()->json([
             'status'    => 'success',
-            'message'   => 'Draft saved at ',
+            'message'   => trans('entities.pages_edit_draft_save_at'),
             'timestamp' => $utcUpdateTimestamp
         ]);
     }
@@ -292,7 +274,7 @@ class PageController extends Controller
      */
     public function redirectFromLink($pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->entityRepo->getById('page', $pageId);
         return redirect($page->getUrl());
     }
 
@@ -304,11 +286,10 @@ class PageController extends Controller
      */
     public function showDelete($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-delete', $page);
-        $this->setPageTitle('Delete Page ' . $page->getShortName());
-        return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
+        $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
+        return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
     }
 
 
@@ -321,11 +302,10 @@ class PageController extends Controller
      */
     public function showDeleteDraft($bookSlug, $pageId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
         $this->checkOwnablePermission('page-update', $page);
-        $this->setPageTitle('Delete Draft Page ' . $page->getShortName());
-        return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
+        $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
+        return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
     }
 
     /**
@@ -337,12 +317,12 @@ class PageController extends Controller
      */
     public function destroy($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $book = $page->book;
         $this->checkOwnablePermission('page-delete', $page);
         Activity::addMessage('page_delete', $book->id, $page->name);
-        session()->flash('success', 'Page deleted');
-        $this->pageRepo->destroy($page);
+        session()->flash('success', trans('entities.pages_delete_success'));
+        $this->entityRepo->destroyPage($page);
         return redirect($book->getUrl());
     }
 
@@ -355,11 +335,11 @@ class PageController extends Controller
      */
     public function destroyDraft($bookSlug, $pageId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getById($pageId, true);
+        $page = $this->entityRepo->getById('page', $pageId, true);
+        $book = $page->book;
         $this->checkOwnablePermission('page-update', $page);
-        session()->flash('success', 'Draft deleted');
-        $this->pageRepo->destroy($page);
+        session()->flash('success', trans('entities.pages_delete_draft_success'));
+        $this->entityRepo->destroyPage($page);
         return redirect($book->getUrl());
     }
 
@@ -371,10 +351,9 @@ class PageController extends Controller
      */
     public function showRevisions($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
-        $this->setPageTitle('Revisions For ' . $page->getShortName());
-        return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+        return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
     }
 
     /**
@@ -386,16 +365,15 @@ class PageController extends Controller
      */
     public function showRevision($bookSlug, $pageSlug, $revisionId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
-        $revision = $this->pageRepo->getRevisionById($revisionId);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $revision = $this->entityRepo->getById('page_revision', $revisionId, false);
 
         $page->fill($revision->toArray());
-        $this->setPageTitle('Page Revision For ' . $page->getShortName());
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
         
         return view('pages/revision', [
             'page' => $page,
-            'book' => $book,
+            'book' => $page->book,
         ]);
     }
 
@@ -408,20 +386,19 @@ class PageController extends Controller
      */
     public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
-        $revision = $this->pageRepo->getRevisionById($revisionId);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+        $revision = $this->entityRepo->getById('page_revision', $revisionId);
 
         $prev = $revision->getPrevious();
         $prevContent = ($prev === null) ? '' : $prev->html;
         $diff = (new Htmldiff)->diff($prevContent, $revision->html);
 
         $page->fill($revision->toArray());
-        $this->setPageTitle('Page Revision For ' . $page->getShortName());
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
 
         return view('pages/revision', [
             'page' => $page,
-            'book' => $book,
+            'book' => $page->book,
             'diff' => $diff,
         ]);
     }
@@ -435,11 +412,10 @@ class PageController extends Controller
      */
     public function restoreRevision($bookSlug, $pageSlug, $revisionId)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
-        Activity::add($page, 'page_restore', $book->id);
+        $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
+        Activity::add($page, 'page_restore', $page->book->id);
         return redirect($page->getUrl());
     }
 
@@ -452,8 +428,7 @@ class PageController extends Controller
      */
     public function exportPdf($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $pdfContent = $this->exportService->pageToPdf($page);
         return response()->make($pdfContent, 200, [
             'Content-Type'        => 'application/octet-stream',
@@ -469,8 +444,7 @@ class PageController extends Controller
      */
     public function exportHtml($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $containedHtml = $this->exportService->pageToContainedHtml($page);
         return response()->make($containedHtml, 200, [
             'Content-Type'        => 'application/octet-stream',
@@ -486,8 +460,7 @@ class PageController extends Controller
      */
     public function exportPlainText($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $containedHtml = $this->exportService->pageToPlainText($page);
         return response()->make($containedHtml, 200, [
             'Content-Type'        => 'application/octet-stream',
@@ -501,9 +474,9 @@ class PageController extends Controller
      */
     public function showRecentlyCreated()
     {
-        $pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created'));
+        $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
         return view('pages/detailed-listing', [
-            'title' => 'Recently Created Pages',
+            'title' => trans('entities.recently_created_pages'),
             'pages' => $pages
         ]);
     }
@@ -514,9 +487,9 @@ class PageController extends Controller
      */
     public function showRecentlyUpdated()
     {
-        $pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
+        $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
         return view('pages/detailed-listing', [
-            'title' => 'Recently Updated Pages',
+            'title' => trans('entities.recently_updated_pages'),
             'pages' => $pages
         ]);
     }
@@ -529,8 +502,7 @@ class PageController extends Controller
      */
     public function showRestrict($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
         $roles = $this->userRepo->getRestrictableRoles();
         return view('pages/restrictions', [
@@ -548,11 +520,10 @@ class PageController extends Controller
      */
     public function showMove($bookSlug, $pageSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
         return view('pages/move', [
-            'book' => $book,
+            'book' => $page->book,
             'page' => $page
         ]);
     }
@@ -567,8 +538,7 @@ class PageController extends Controller
      */
     public function move($bookSlug, $pageSlug, Request $request)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
 
         $entitySelection = $request->get('entity_selection', null);
@@ -580,22 +550,17 @@ class PageController extends Controller
         $entityType = $stringExploded[0];
         $entityId = intval($stringExploded[1]);
 
-        $parent = false;
 
-        if ($entityType == 'chapter') {
-            $parent = $this->chapterRepo->getById($entityId);
-        } else if ($entityType == 'book') {
-            $parent = $this->bookRepo->getById($entityId);
-        }
-
-        if ($parent === false || $parent === null) {
-            session()->flash('The selected Book or Chapter was not found');
+        try {
+            $parent = $this->entityRepo->getById($entityType, $entityId);
+        } catch (\Exception $e) {
+            session()->flash(trans('entities.selected_book_chapter_not_found'));
             return redirect()->back();
         }
 
-        $this->pageRepo->changePageParent($page, $parent);
+        $this->entityRepo->changePageParent($page, $parent);
         Activity::add($page, 'page_move', $page->book->id);
-        session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
+        session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
 
         return redirect($page->getUrl());
     }
@@ -609,11 +574,10 @@ class PageController extends Controller
      */
     public function restrict($bookSlug, $pageSlug, Request $request)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
-        $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
-        session()->flash('success', 'Page Permissions Updated');
+        $this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
+        session()->flash('success', trans('entities.pages_permissions_success'));
         return redirect($page->getUrl());
     }
 
diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php
index ed430c0b7..cd064e7e8 100644
--- a/app/Http/Controllers/PermissionController.php
+++ b/app/Http/Controllers/PermissionController.php
@@ -2,9 +2,7 @@
 
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Repos\PermissionsRepo;
-use BookStack\Services\PermissionService;
 use Illuminate\Http\Request;
-use BookStack\Http\Requests;
 
 class PermissionController extends Controller
 {
@@ -55,7 +53,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->saveNewRole($request->all());
-        session()->flash('success', 'Role successfully created');
+        session()->flash('success', trans('settings.role_create_success'));
         return redirect('/settings/roles');
     }
 
@@ -69,7 +67,7 @@ class PermissionController extends Controller
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
-        if ($role->hidden) throw new PermissionsException('This role cannot be edited');
+        if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
         return view('settings/roles/edit', ['role' => $role]);
     }
 
@@ -88,7 +86,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->updateRole($id, $request->all());
-        session()->flash('success', 'Role successfully updated');
+        session()->flash('success', trans('settings.role_update_success'));
         return redirect('/settings/roles');
     }
 
@@ -103,7 +101,7 @@ class PermissionController extends Controller
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
         $roles = $this->permissionsRepo->getAllRolesExcept($role);
-        $blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
+        $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
         $roles->prepend($blankRole);
         return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
     }
@@ -126,7 +124,7 @@ class PermissionController extends Controller
             return redirect()->back();
         }
 
-        session()->flash('success', 'Role successfully deleted');
+        session()->flash('success', trans('settings.role_delete_success'));
         return redirect('/settings/roles');
     }
 }
diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php
index 58ad737c4..37aaccece 100644
--- a/app/Http/Controllers/SearchController.php
+++ b/app/Http/Controllers/SearchController.php
@@ -1,34 +1,22 @@
-<?php
-
-namespace BookStack\Http\Controllers;
+<?php namespace BookStack\Http\Controllers;
 
+use BookStack\Repos\EntityRepo;
 use BookStack\Services\ViewService;
 use Illuminate\Http\Request;
 
-use BookStack\Http\Requests;
-use BookStack\Repos\BookRepo;
-use BookStack\Repos\ChapterRepo;
-use BookStack\Repos\PageRepo;
-
 class SearchController extends Controller
 {
-    protected $pageRepo;
-    protected $bookRepo;
-    protected $chapterRepo;
+    protected $entityRepo;
     protected $viewService;
 
     /**
      * SearchController constructor.
-     * @param PageRepo $pageRepo
-     * @param BookRepo $bookRepo
-     * @param ChapterRepo $chapterRepo
+     * @param EntityRepo $entityRepo
      * @param ViewService $viewService
      */
-    public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
+    public function __construct(EntityRepo $entityRepo, ViewService $viewService)
     {
-        $this->pageRepo = $pageRepo;
-        $this->bookRepo = $bookRepo;
-        $this->chapterRepo = $chapterRepo;
+        $this->entityRepo = $entityRepo;
         $this->viewService = $viewService;
         parent::__construct();
     }
@@ -46,10 +34,10 @@ class SearchController extends Controller
         }
         $searchTerm = $request->get('term');
         $paginationAppends = $request->only('term');
-        $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
-        $books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
-        $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
-        $this->setPageTitle('Search For ' . $searchTerm);
+        $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
+        $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
+        $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
+        $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
         return view('search/all', [
             'pages'      => $pages,
             'books'      => $books,
@@ -69,11 +57,11 @@ class SearchController extends Controller
 
         $searchTerm = $request->get('term');
         $paginationAppends = $request->only('term');
-        $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
-        $this->setPageTitle('Page Search For ' . $searchTerm);
+        $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
+        $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
         return view('search/entity-search-list', [
             'entities'   => $pages,
-            'title'      => 'Page Search Results',
+            'title'      => trans('entities.search_results_page'),
             'searchTerm' => $searchTerm
         ]);
     }
@@ -89,11 +77,11 @@ class SearchController extends Controller
 
         $searchTerm = $request->get('term');
         $paginationAppends = $request->only('term');
-        $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
-        $this->setPageTitle('Chapter Search For ' . $searchTerm);
+        $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
+        $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
         return view('search/entity-search-list', [
             'entities'   => $chapters,
-            'title'      => 'Chapter Search Results',
+            'title'      => trans('entities.search_results_chapter'),
             'searchTerm' => $searchTerm
         ]);
     }
@@ -109,11 +97,11 @@ class SearchController extends Controller
 
         $searchTerm = $request->get('term');
         $paginationAppends = $request->only('term');
-        $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
-        $this->setPageTitle('Book Search For ' . $searchTerm);
+        $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
+        $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
         return view('search/entity-search-list', [
             'entities'   => $books,
-            'title'      => 'Book Search Results',
+            'title'      => trans('entities.search_results_book'),
             'searchTerm' => $searchTerm
         ]);
     }
@@ -132,8 +120,8 @@ class SearchController extends Controller
         }
         $searchTerm = $request->get('term');
         $searchWhereTerms = [['book_id', '=', $bookId]];
-        $pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
-        $chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
+        $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
+        $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
         return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
     }
 
@@ -152,9 +140,11 @@ class SearchController extends Controller
 
         // Search for entities otherwise show most popular
         if ($searchTerm !== false) {
-            if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
-            if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
-            if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
+            foreach (['page', 'chapter', 'book'] as $entityType) {
+                if ($entityTypes->contains($entityType)) {
+                    $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
+                }
+            }
             $entities = $entities->sortByDesc('title_relevance');
         } else {
             $entityNames = $entityTypes->map(function ($type) {
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 65135eda3..70a12631a 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -1,8 +1,7 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Illuminate\Http\Request;
-
-use BookStack\Http\Requests;
+use Illuminate\Http\Response;
 use Setting;
 
 class SettingController extends Controller
@@ -39,7 +38,7 @@ class SettingController extends Controller
             Setting::put($key, $value);
         }
 
-        session()->flash('success', 'Settings Saved');
+        session()->flash('success', trans('settings.settings_save_success'));
         return redirect('/settings');
     }
 
diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php
index c8a356541..24bdcdb1c 100644
--- a/app/Http/Controllers/TagController.php
+++ b/app/Http/Controllers/TagController.php
@@ -2,7 +2,6 @@
 
 use BookStack\Repos\TagRepo;
 use Illuminate\Http\Request;
-use BookStack\Http\Requests;
 
 class TagController extends Controller
 {
@@ -16,12 +15,14 @@ class TagController extends Controller
     public function __construct(TagRepo $tagRepo)
     {
         $this->tagRepo = $tagRepo;
+        parent::__construct();
     }
 
     /**
      * Get all the Tags for a particular entity
      * @param $entityType
      * @param $entityId
+     * @return \Illuminate\Http\JsonResponse
      */
     public function getForEntity($entityType, $entityId)
     {
@@ -29,29 +30,10 @@ class TagController extends Controller
         return response()->json($tags);
     }
 
-    /**
-     * Update the tags for a particular entity.
-     * @param $entityType
-     * @param $entityId
-     * @param Request $request
-     * @return mixed
-     */
-    public function updateForEntity($entityType, $entityId, Request $request)
-    {
-        $entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
-        if ($entity === null) return $this->jsonError("Entity not found", 404);
-
-        $inputTags = $request->input('tags');
-        $tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
-        return response()->json([
-            'tags' => $tags,
-            'message' => 'Tags successfully updated'
-        ]);
-    }
-
     /**
      * Get tag name suggestions from a given search term.
      * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
      */
     public function getNameSuggestions(Request $request)
     {
@@ -63,6 +45,7 @@ class TagController extends Controller
     /**
      * Get tag value suggestions from a given search term.
      * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
      */
     public function getValueSuggestions(Request $request)
     {
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 18ef1a671..b5184c40a 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -44,7 +44,7 @@ class UserController extends Controller
             'sort' => $request->has('sort') ? $request->get('sort') : 'name',
         ];
         $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
-        $this->setPageTitle('Users');
+        $this->setPageTitle(trans('settings.users'));
         $users->appends($listDetails);
         return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
     }
@@ -83,7 +83,6 @@ class UserController extends Controller
         }
         $this->validate($request, $validationRules);
 
-
         $user = $this->user->fill($request->all());
 
         if ($authMethod === 'standard') {
@@ -131,7 +130,7 @@ class UserController extends Controller
         $authMethod = ($user->system_name) ? 'system' : config('auth.method');
 
         $activeSocialDrivers = $socialAuthService->getActiveDrivers();
-        $this->setPageTitle('User Profile');
+        $this->setPageTitle(trans('settings.user_profile'));
         $roles = $this->userRepo->getAllRoles();
         return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
     }
@@ -154,8 +153,6 @@ class UserController extends Controller
             'email'            => 'min:2|email|unique:users,email,' . $id,
             'password'         => 'min:5|required_with:password_confirm',
             'password-confirm' => 'same:password|required_with:password'
-        ], [
-            'password-confirm.required_with' => 'Password confirmation required'
         ]);
 
         $user = $this->user->findOrFail($id);
@@ -179,7 +176,7 @@ class UserController extends Controller
         }
 
         $user->save();
-        session()->flash('success', 'User successfully updated');
+        session()->flash('success', trans('settings.users_edit_success'));
 
         $redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
         return redirect($redirectUrl);
@@ -197,7 +194,7 @@ class UserController extends Controller
         });
 
         $user = $this->user->findOrFail($id);
-        $this->setPageTitle('Delete User ' . $user->name);
+        $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
         return view('users/delete', ['user' => $user]);
     }
 
@@ -216,17 +213,17 @@ class UserController extends Controller
         $user = $this->userRepo->getById($id);
 
         if ($this->userRepo->isOnlyAdmin($user)) {
-            session()->flash('error', 'You cannot delete the only admin');
+            session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
             return redirect($user->getEditUrl());
         }
 
         if ($user->system_name === 'public') {
-            session()->flash('error', 'You cannot delete the guest user');
+            session()->flash('error', trans('errors.users_cannot_delete_guest'));
             return redirect($user->getEditUrl());
         }
 
         $this->userRepo->destroy($user);
-        session()->flash('success', 'User successfully removed');
+        session()->flash('success', trans('settings.users_delete_success'));
 
         return redirect('/settings/users');
     }
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index 8461ed0ba..b78016688 100644
--- a/app/Http/Middleware/Authenticate.php
+++ b/app/Http/Middleware/Authenticate.php
@@ -4,8 +4,6 @@ namespace BookStack\Http\Middleware;
 
 use Closure;
 use Illuminate\Contracts\Auth\Guard;
-use BookStack\Exceptions\UserRegistrationException;
-use Setting;
 
 class Authenticate
 {
diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php
index 646030a10..affd8f076 100644
--- a/app/Notifications/ResetPassword.php
+++ b/app/Notifications/ResetPassword.php
@@ -43,8 +43,9 @@ class ResetPassword extends Notification
     public function toMail()
     {
         return (new MailMessage)
-            ->line('You are receiving this email because we received a password reset request for your account.')
-            ->action('Reset Password', baseUrl('password/reset/' . $this->token))
-            ->line('If you did not request a password reset, no further action is required.');
+            ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
+            ->line(trans('auth.email_reset_text'))
+            ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
+            ->line(trans('auth.email_reset_not_requested'));
     }
 }
diff --git a/app/Page.php b/app/Page.php
index 3ee9e90f4..b24e7778a 100644
--- a/app/Page.php
+++ b/app/Page.php
@@ -7,6 +7,10 @@ class Page extends Entity
 
     protected $simpleAttributes = ['name', 'id', 'slug'];
 
+    protected $with = ['book'];
+
+    protected $fieldsToSearch = ['name', 'text'];
+
     /**
      * Converts this page into a simplified array.
      * @return mixed
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 4665bf6c7..66f40569a 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -1,6 +1,8 @@
 <?php namespace BookStack\Providers;
 
+use Carbon\Carbon;
 use Illuminate\Support\ServiceProvider;
+use Validator;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -12,11 +14,12 @@ class AppServiceProvider extends ServiceProvider
     public function boot()
     {
         // Custom validation methods
-        \Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
+        Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
             $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
             return in_array($value->getMimeType(), $imageMimes);
         });
 
+        Carbon::setLocale(config('app.locale'));
     }
 
     /**
diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php
deleted file mode 100644
index 7bb91f472..000000000
--- a/app/Repos/BookRepo.php
+++ /dev/null
@@ -1,295 +0,0 @@
-<?php namespace BookStack\Repos;
-
-use Alpha\B;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Support\Str;
-use BookStack\Book;
-use Views;
-
-class BookRepo extends EntityRepo
-{
-    protected $pageRepo;
-    protected $chapterRepo;
-
-    /**
-     * BookRepo constructor.
-     * @param PageRepo $pageRepo
-     * @param ChapterRepo $chapterRepo
-     */
-    public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
-    {
-        $this->pageRepo = $pageRepo;
-        $this->chapterRepo = $chapterRepo;
-        parent::__construct();
-    }
-
-    /**
-     * Base query for getting books.
-     * Takes into account any restrictions.
-     * @return mixed
-     */
-    private function bookQuery()
-    {
-        return $this->permissionService->enforceBookRestrictions($this->book, 'view');
-    }
-
-    /**
-     * Get the book that has the given id.
-     * @param $id
-     * @return mixed
-     */
-    public function getById($id)
-    {
-        return $this->bookQuery()->findOrFail($id);
-    }
-
-    /**
-     * Get all books, Limited by count.
-     * @param int $count
-     * @return mixed
-     */
-    public function getAll($count = 10)
-    {
-        $bookQuery = $this->bookQuery()->orderBy('name', 'asc');
-        if (!$count) return $bookQuery->get();
-        return $bookQuery->take($count)->get();
-    }
-
-    /**
-     * Get all books paginated.
-     * @param int $count
-     * @return mixed
-     */
-    public function getAllPaginated($count = 10)
-    {
-        return $this->bookQuery()
-            ->orderBy('name', 'asc')->paginate($count);
-    }
-
-
-    /**
-     * Get the latest books.
-     * @param int $count
-     * @return mixed
-     */
-    public function getLatest($count = 10)
-    {
-        return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
-    }
-
-    /**
-     * Gets the most recently viewed for a user.
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getRecentlyViewed($count = 10, $page = 0)
-    {
-        return Views::getUserRecentlyViewed($count, $page, $this->book);
-    }
-
-    /**
-     * Gets the most viewed books.
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getPopular($count = 10, $page = 0)
-    {
-        return Views::getPopular($count, $page, $this->book);
-    }
-
-    /**
-     * Get a book by slug
-     * @param $slug
-     * @return mixed
-     * @throws NotFoundException
-     */
-    public function getBySlug($slug)
-    {
-        $book = $this->bookQuery()->where('slug', '=', $slug)->first();
-        if ($book === null) throw new NotFoundException('Book not found');
-        return $book;
-    }
-
-    /**
-     * Checks if a book exists.
-     * @param $id
-     * @return bool
-     */
-    public function exists($id)
-    {
-        return $this->bookQuery()->where('id', '=', $id)->exists();
-    }
-
-    /**
-     * Get a new book instance from request input.
-     * @param array $input
-     * @return Book
-     */
-    public function createFromInput($input)
-    {
-        $book = $this->book->newInstance($input);
-        $book->slug = $this->findSuitableSlug($book->name);
-        $book->created_by = user()->id;
-        $book->updated_by = user()->id;
-        $book->save();
-        $this->permissionService->buildJointPermissionsForEntity($book);
-        return $book;
-    }
-
-    /**
-     * Update the given book from user input.
-     * @param Book $book
-     * @param $input
-     * @return Book
-     */
-    public function updateFromInput(Book $book, $input)
-    {
-        if ($book->name !== $input['name']) {
-            $book->slug = $this->findSuitableSlug($input['name'], $book->id);
-        }
-        $book->fill($input);
-        $book->updated_by = user()->id;
-        $book->save();
-        $this->permissionService->buildJointPermissionsForEntity($book);
-        return $book;
-    }
-
-    /**
-     * Destroy the given book.
-     * @param Book $book
-     * @throws \Exception
-     */
-    public function destroy(Book $book)
-    {
-        foreach ($book->pages as $page) {
-            $this->pageRepo->destroy($page);
-        }
-        foreach ($book->chapters as $chapter) {
-            $this->chapterRepo->destroy($chapter);
-        }
-        $book->views()->delete();
-        $book->permissions()->delete();
-        $this->permissionService->deleteJointPermissionsForEntity($book);
-        $book->delete();
-    }
-
-    /**
-     * Get the next child element priority.
-     * @param Book $book
-     * @return int
-     */
-    public function getNewPriority($book)
-    {
-        $lastElem = $this->getChildren($book)->pop();
-        return $lastElem ? $lastElem->priority + 1 : 0;
-    }
-
-    /**
-     * @param string $slug
-     * @param bool|false $currentId
-     * @return bool
-     */
-    public function doesSlugExist($slug, $currentId = false)
-    {
-        $query = $this->book->where('slug', '=', $slug);
-        if ($currentId) {
-            $query = $query->where('id', '!=', $currentId);
-        }
-        return $query->count() > 0;
-    }
-
-    /**
-     * Provides a suitable slug for the given book name.
-     * Ensures the returned slug is unique in the system.
-     * @param string $name
-     * @param bool|false $currentId
-     * @return string
-     */
-    public function findSuitableSlug($name, $currentId = false)
-    {
-        $slug = $this->nameToSlug($name);
-        while ($this->doesSlugExist($slug, $currentId)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
-        }
-        return $slug;
-    }
-
-    /**
-     * Get all child objects of a book.
-     * Returns a sorted collection of Pages and Chapters.
-     * Loads the book slug onto child elements to prevent access database access for getting the slug.
-     * @param Book $book
-     * @param bool $filterDrafts
-     * @return mixed
-     */
-    public function getChildren(Book $book, $filterDrafts = false)
-    {
-        $pageQuery = $book->pages()->where('chapter_id', '=', 0);
-        $pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
-
-        if ($filterDrafts) {
-            $pageQuery = $pageQuery->where('draft', '=', false);
-        }
-
-        $pages = $pageQuery->get();
-
-        $chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
-            $this->permissionService->enforcePageRestrictions($query, 'view');
-            if ($filterDrafts) $query->where('draft', '=', false);
-        }]);
-        $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
-        $chapters = $chapterQuery->get();
-        $children = $pages->values();
-        foreach ($chapters as $chapter) {
-            $children->push($chapter);
-        }
-        $bookSlug = $book->slug;
-
-        $children->each(function ($child) use ($bookSlug) {
-            $child->setAttribute('bookSlug', $bookSlug);
-            if ($child->isA('chapter')) {
-                $child->pages->each(function ($page) use ($bookSlug) {
-                    $page->setAttribute('bookSlug', $bookSlug);
-                });
-                $child->pages = $child->pages->sortBy(function ($child, $key) {
-                    $score = $child->priority;
-                    if ($child->draft) $score -= 100;
-                    return $score;
-                });
-            }
-        });
-
-        // Sort items with drafts first then by priority.
-        return $children->sortBy(function ($child, $key) {
-            $score = $child->priority;
-            if ($child->isA('page') && $child->draft) $score -= 100;
-            return $score;
-        });
-    }
-
-    /**
-     * Get books by search term.
-     * @param $term
-     * @param int $count
-     * @param array $paginationAppends
-     * @return mixed
-     */
-    public function getBySearch($term, $count = 20, $paginationAppends = [])
-    {
-        $terms = $this->prepareSearchTerms($term);
-        $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
-        $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
-        $books = $bookQuery->paginate($count)->appends($paginationAppends);
-        $words = join('|', explode(' ', preg_quote(trim($term), '/')));
-        foreach ($books as $book) {
-            //highlight
-            $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
-            $book->searchSnippet = $result;
-        }
-        return $books;
-    }
-
-}
\ No newline at end of file
diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php
deleted file mode 100644
index 4c13b9aaf..000000000
--- a/app/Repos/ChapterRepo.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php namespace BookStack\Repos;
-
-
-use Activity;
-use BookStack\Book;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Support\Str;
-use BookStack\Chapter;
-
-class ChapterRepo extends EntityRepo
-{
-    protected $pageRepo;
-
-    /**
-     * ChapterRepo constructor.
-     * @param $pageRepo
-     */
-    public function __construct(PageRepo $pageRepo)
-    {
-        $this->pageRepo = $pageRepo;
-        parent::__construct();
-    }
-
-    /**
-     * Base query for getting chapters, Takes permissions into account.
-     * @return mixed
-     */
-    private function chapterQuery()
-    {
-        return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
-    }
-
-    /**
-     * Check if an id exists.
-     * @param $id
-     * @return bool
-     */
-    public function idExists($id)
-    {
-        return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
-    }
-
-    /**
-     * Get a chapter by a specific id.
-     * @param $id
-     * @return mixed
-     */
-    public function getById($id)
-    {
-        return $this->chapterQuery()->findOrFail($id);
-    }
-
-    /**
-     * Get all chapters.
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
-     */
-    public function getAll()
-    {
-        return $this->chapterQuery()->all();
-    }
-
-    /**
-     * Get a chapter that has the given slug within the given book.
-     * @param $slug
-     * @param $bookId
-     * @return mixed
-     * @throws NotFoundException
-     */
-    public function getBySlug($slug, $bookId)
-    {
-        $chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
-        if ($chapter === null) throw new NotFoundException('Chapter not found');
-        return $chapter;
-    }
-
-    /**
-     * Get the child items for a chapter
-     * @param Chapter $chapter
-     */
-    public function getChildren(Chapter $chapter)
-    {
-        $pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
-        // Sort items with drafts first then by priority.
-        return $pages->sortBy(function ($child, $key) {
-            $score = $child->priority;
-            if ($child->draft) $score -= 100;
-            return $score;
-        });
-    }
-
-    /**
-     * Create a new chapter from request input.
-     * @param $input
-     * @param Book $book
-     * @return Chapter
-     */
-    public function createFromInput($input, Book $book)
-    {
-        $chapter = $this->chapter->newInstance($input);
-        $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
-        $chapter->created_by = user()->id;
-        $chapter->updated_by = user()->id;
-        $chapter = $book->chapters()->save($chapter);
-        $this->permissionService->buildJointPermissionsForEntity($chapter);
-        return $chapter;
-    }
-
-    /**
-     * Destroy a chapter and its relations by providing its slug.
-     * @param Chapter $chapter
-     */
-    public function destroy(Chapter $chapter)
-    {
-        if (count($chapter->pages) > 0) {
-            foreach ($chapter->pages as $page) {
-                $page->chapter_id = 0;
-                $page->save();
-            }
-        }
-        Activity::removeEntity($chapter);
-        $chapter->views()->delete();
-        $chapter->permissions()->delete();
-        $this->permissionService->deleteJointPermissionsForEntity($chapter);
-        $chapter->delete();
-    }
-
-    /**
-     * Check if a chapter's slug exists.
-     * @param            $slug
-     * @param            $bookId
-     * @param bool|false $currentId
-     * @return bool
-     */
-    public function doesSlugExist($slug, $bookId, $currentId = false)
-    {
-        $query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
-        if ($currentId) {
-            $query = $query->where('id', '!=', $currentId);
-        }
-        return $query->count() > 0;
-    }
-
-    /**
-     * Finds a suitable slug for the provided name.
-     * Checks database to prevent duplicate slugs.
-     * @param            $name
-     * @param            $bookId
-     * @param bool|false $currentId
-     * @return string
-     */
-    public function findSuitableSlug($name, $bookId, $currentId = false)
-    {
-        $slug = $this->nameToSlug($name);
-        while ($this->doesSlugExist($slug, $bookId, $currentId)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
-        }
-        return $slug;
-    }
-
-    /**
-     * Get a new priority value for a new page to be added
-     * to the given chapter.
-     * @param Chapter $chapter
-     * @return int
-     */
-    public function getNewPriority(Chapter $chapter)
-    {
-        $lastPage = $chapter->pages->last();
-        return $lastPage !== null ? $lastPage->priority + 1 : 0;
-    }
-
-    /**
-     * Get chapters by the given search term.
-     * @param string $term
-     * @param array $whereTerms
-     * @param int $count
-     * @param array $paginationAppends
-     * @return mixed
-     */
-    public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
-    {
-        $terms = $this->prepareSearchTerms($term);
-        $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
-        $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
-        $chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
-        $words = join('|', explode(' ', preg_quote(trim($term), '/')));
-        foreach ($chapters as $chapter) {
-            //highlight
-            $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
-            $chapter->searchSnippet = $result;
-        }
-        return $chapters;
-    }
-
-    /**
-     * Changes the book relation of this chapter.
-     * @param $bookId
-     * @param Chapter $chapter
-     * @param bool $rebuildPermissions
-     * @return Chapter
-     */
-    public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
-    {
-        $chapter->book_id = $bookId;
-        // Update related activity
-        foreach ($chapter->activity as $activity) {
-            $activity->book_id = $bookId;
-            $activity->save();
-        }
-        $chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
-        $chapter->save();
-        // Update all child pages
-        foreach ($chapter->pages as $page) {
-            $this->pageRepo->changeBook($bookId, $page);
-        }
-
-        // Update permissions if applicable
-        if ($rebuildPermissions) {
-            $chapter->load('book');
-            $this->permissionService->buildJointPermissionsForEntity($chapter->book);
-        }
-
-        return $chapter;
-    }
-
-}
\ No newline at end of file
diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php
index 7ecfb758c..0515a4cd4 100644
--- a/app/Repos/EntityRepo.php
+++ b/app/Repos/EntityRepo.php
@@ -3,11 +3,16 @@
 use BookStack\Book;
 use BookStack\Chapter;
 use BookStack\Entity;
+use BookStack\Exceptions\NotFoundException;
 use BookStack\Page;
+use BookStack\PageRevision;
+use BookStack\Services\AttachmentService;
 use BookStack\Services\PermissionService;
-use BookStack\User;
+use BookStack\Services\ViewService;
+use Carbon\Carbon;
+use DOMDocument;
+use DOMXPath;
 use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Log;
 
 class EntityRepo
 {
@@ -27,11 +32,32 @@ class EntityRepo
      */
     public $page;
 
+    /**
+     * @var PageRevision
+     */
+    protected $pageRevision;
+
+    /**
+     * Base entity instances keyed by type
+     * @var []Entity
+     */
+    protected $entities;
+
     /**
      * @var PermissionService
      */
     protected $permissionService;
 
+    /**
+     * @var ViewService
+     */
+    protected $viewService;
+
+    /**
+     * @var TagRepo
+     */
+    protected $tagRepo;
+
     /**
      * Acceptable operators to be used in a query
      * @var array
@@ -40,26 +66,163 @@ class EntityRepo
 
     /**
      * EntityService constructor.
+     * @param Book $book
+     * @param Chapter $chapter
+     * @param Page $page
+     * @param PageRevision $pageRevision
+     * @param ViewService $viewService
+     * @param PermissionService $permissionService
+     * @param TagRepo $tagRepo
      */
-    public function __construct()
+    public function __construct(
+        Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
+        ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo
+    )
     {
-        $this->book = app(Book::class);
-        $this->chapter = app(Chapter::class);
-        $this->page = app(Page::class);
-        $this->permissionService = app(PermissionService::class);
+        $this->book = $book;
+        $this->chapter = $chapter;
+        $this->page = $page;
+        $this->pageRevision = $pageRevision;
+        $this->entities = [
+            'page' => $this->page,
+            'chapter' => $this->chapter,
+            'book' => $this->book,
+            'page_revision' => $this->pageRevision
+        ];
+        $this->viewService = $viewService;
+        $this->permissionService = $permissionService;
+        $this->tagRepo = $tagRepo;
     }
 
     /**
-     * Get the latest books added to the system.
+     * Get an entity instance via type.
+     * @param $type
+     * @return Entity
+     */
+    protected function getEntity($type)
+    {
+        return $this->entities[strtolower($type)];
+    }
+
+    /**
+     * Base query for searching entities via permission system
+     * @param string $type
+     * @param bool $allowDrafts
+     * @return \Illuminate\Database\Query\Builder
+     */
+    protected function entityQuery($type, $allowDrafts = false)
+    {
+        $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
+        if (strtolower($type) === 'page' && !$allowDrafts) {
+            $q = $q->where('draft', '=', false);
+        }
+        return $q;
+    }
+
+    /**
+     * Check if an entity with the given id exists.
+     * @param $type
+     * @param $id
+     * @return bool
+     */
+    public function exists($type, $id)
+    {
+        return $this->entityQuery($type)->where('id', '=', $id)->exists();
+    }
+
+    /**
+     * Get an entity by ID
+     * @param string $type
+     * @param integer $id
+     * @param bool $allowDrafts
+     * @return Entity
+     */
+    public function getById($type, $id, $allowDrafts = false)
+    {
+        return $this->entityQuery($type, $allowDrafts)->findOrFail($id);
+    }
+
+    /**
+     * Get an entity by its url slug.
+     * @param string $type
+     * @param string $slug
+     * @param string|bool $bookSlug
+     * @return Entity
+     * @throws NotFoundException
+     */
+    public function getBySlug($type, $slug, $bookSlug = false)
+    {
+        $q = $this->entityQuery($type)->where('slug', '=', $slug);
+
+        if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
+            $q = $q->where('book_id', '=', function($query) use ($bookSlug) {
+                $query->select('id')
+                    ->from($this->book->getTable())
+                    ->where('slug', '=', $bookSlug)->limit(1);
+            });
+        }
+        $entity = $q->first();
+        if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
+        return $entity;
+    }
+
+
+    /**
+     * Search through page revisions and retrieve the last page in the
+     * current book that has a slug equal to the one given.
+     * @param string $pageSlug
+     * @param string $bookSlug
+     * @return null|Page
+     */
+    public function getPageByOldSlug($pageSlug, $bookSlug)
+    {
+        $revision = $this->pageRevision->where('slug', '=', $pageSlug)
+            ->whereHas('page', function ($query) {
+                $this->permissionService->enforceEntityRestrictions('page', $query);
+            })
+            ->where('type', '=', 'version')
+            ->where('book_slug', '=', $bookSlug)
+            ->orderBy('created_at', 'desc')
+            ->with('page')->first();
+        return $revision !== null ? $revision->page : null;
+    }
+
+    /**
+     * Get all entities of a type limited by count unless count if false.
+     * @param string $type
+     * @param integer|bool $count
+     * @return Collection
+     */
+    public function getAll($type, $count = 20)
+    {
+        $q = $this->entityQuery($type)->orderBy('name', 'asc');
+        if ($count !== false) $q = $q->take($count);
+        return $q->get();
+    }
+
+    /**
+     * Get all entities in a paginated format
+     * @param $type
+     * @param int $count
+     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+     */
+    public function getAllPaginated($type, $count = 10)
+    {
+        return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
+    }
+
+    /**
+     * Get the most recently created entities of the given type.
+     * @param string $type
      * @param int $count
      * @param int $page
-     * @param bool $additionalQuery
-     * @return
+     * @param bool|callable $additionalQuery
      */
-    public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
+    public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
     {
-        $query = $this->permissionService->enforceBookRestrictions($this->book)
+        $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
             ->orderBy('created_at', 'desc');
+        if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
         if ($additionalQuery !== false && is_callable($additionalQuery)) {
             $additionalQuery($query);
         }
@@ -67,45 +230,17 @@ class EntityRepo
     }
 
     /**
-     * Get the most recently updated books.
-     * @param $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getRecentlyUpdatedBooks($count = 20, $page = 0)
-    {
-        return $this->permissionService->enforceBookRestrictions($this->book)
-            ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the latest pages added to the system.
+     * Get the most recently updated entities of the given type.
+     * @param string $type
      * @param int $count
      * @param int $page
-     * @param bool $additionalQuery
-     * @return
+     * @param bool|callable $additionalQuery
      */
-    public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
+    public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
     {
-        $query = $this->permissionService->enforcePageRestrictions($this->page)
-            ->orderBy('created_at', 'desc')->where('draft', '=', false);
-        if ($additionalQuery !== false && is_callable($additionalQuery)) {
-            $additionalQuery($query);
-        }
-        return $query->with('book')->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the latest chapters added to the system.
-     * @param int $count
-     * @param int $page
-     * @param bool $additionalQuery
-     * @return
-     */
-    public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
-    {
-        $query = $this->permissionService->enforceChapterRestrictions($this->chapter)
-            ->orderBy('created_at', 'desc');
+        $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
+            ->orderBy('updated_at', 'desc');
+        if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
         if ($additionalQuery !== false && is_callable($additionalQuery)) {
             $additionalQuery($query);
         }
@@ -113,16 +248,51 @@ class EntityRepo
     }
 
     /**
-     * Get the most recently updated pages.
-     * @param $count
+     * Get the most recently viewed entities.
+     * @param string|bool $type
+     * @param int $count
      * @param int $page
      * @return mixed
      */
-    public function getRecentlyUpdatedPages($count = 20, $page = 0)
+    public function getRecentlyViewed($type, $count = 10, $page = 0)
     {
-        return $this->permissionService->enforcePageRestrictions($this->page)
-            ->where('draft', '=', false)
-            ->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
+        $filter = is_bool($type) ? false : $this->getEntity($type);
+        return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
+    }
+
+    /**
+     * Get the latest pages added to the system with pagination.
+     * @param string $type
+     * @param int $count
+     * @return mixed
+     */
+    public function getRecentlyCreatedPaginated($type, $count = 20)
+    {
+        return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
+    }
+
+    /**
+     * Get the latest pages added to the system with pagination.
+     * @param string $type
+     * @param int $count
+     * @return mixed
+     */
+    public function getRecentlyUpdatedPaginated($type, $count = 20)
+    {
+        return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
+    }
+
+    /**
+     * Get the most popular entities base on all views.
+     * @param string|bool $type
+     * @param int $count
+     * @param int $page
+     * @return mixed
+     */
+    public function getPopular($type, $count = 10, $page = 0)
+    {
+        $filter = is_bool($type) ? false : $this->getEntity($type);
+        return $this->viewService->getPopular($count, $page, $filter);
     }
 
     /**
@@ -138,6 +308,163 @@ class EntityRepo
             ->skip($count * $page)->take($count)->get();
     }
 
+    /**
+     * Get all child objects of a book.
+     * Returns a sorted collection of Pages and Chapters.
+     * Loads the book slug onto child elements to prevent access database access for getting the slug.
+     * @param Book $book
+     * @param bool $filterDrafts
+     * @return mixed
+     */
+    public function getBookChildren(Book $book, $filterDrafts = false)
+    {
+        $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts);
+        $entities = [];
+        $parents = [];
+        $tree = [];
+
+        foreach ($q as $index => $rawEntity) {
+            if ($rawEntity->entity_type === 'Bookstack\\Page') {
+                $entities[$index] = $this->page->newFromBuilder($rawEntity);
+            } else if ($rawEntity->entity_type === 'Bookstack\\Chapter') {
+                $entities[$index] = $this->chapter->newFromBuilder($rawEntity);
+                $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
+                $parents[$key] = $entities[$index];
+                $parents[$key]->setAttribute('pages', collect());
+            }
+            if ($entities[$index]->chapter_id === 0) $tree[] = $entities[$index];
+            $entities[$index]->book = $book;
+        }
+
+        foreach ($entities as $entity) {
+            if ($entity->chapter_id === 0) continue;
+            $parentKey = 'Bookstack\\Chapter:' . $entity->chapter_id;
+            $chapter = $parents[$parentKey];
+            $chapter->pages->push($entity);
+        }
+
+        return collect($tree);
+    }
+
+    /**
+     * Get the child items for a chapter sorted by priority but
+     * with draft items floated to the top.
+     * @param Chapter $chapter
+     */
+    public function getChapterChildren(Chapter $chapter)
+    {
+        return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
+            ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
+    }
+
+    /**
+     * Search entities of a type via a given query.
+     * @param string $type
+     * @param string $term
+     * @param array $whereTerms
+     * @param int $count
+     * @param array $paginationAppends
+     * @return mixed
+     */
+    public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
+    {
+        $terms = $this->prepareSearchTerms($term);
+        $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
+        $q = $this->addAdvancedSearchQueries($q, $term);
+        $entities = $q->paginate($count)->appends($paginationAppends);
+        $words = join('|', explode(' ', preg_quote(trim($term), '/')));
+
+        // Highlight page content
+        if ($type === 'page') {
+            //lookahead/behind assertions ensures cut between words
+            $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
+
+            foreach ($entities as $page) {
+                preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
+                //delimiter between occurrences
+                $results = [];
+                foreach ($matches as $line) {
+                    $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
+                }
+                $matchLimit = 6;
+                if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
+                $result = join('... ', $results);
+
+                //highlight
+                $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
+                if (strlen($result) < 5) $result = $page->getExcerpt(80);
+
+                $page->searchSnippet = $result;
+            }
+            return $entities;
+        }
+
+        // Highlight chapter/book content
+        foreach ($entities as $entity) {
+            //highlight
+            $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
+            $entity->searchSnippet = $result;
+        }
+        return $entities;
+    }
+
+    /**
+     * Get the next sequential priority for a new child element in the given book.
+     * @param Book $book
+     * @return int
+     */
+    public function getNewBookPriority(Book $book)
+    {
+        $lastElem = $this->getBookChildren($book)->pop();
+        return $lastElem ? $lastElem->priority + 1 : 0;
+    }
+
+    /**
+     * Get a new priority for a new page to be added to the given chapter.
+     * @param Chapter $chapter
+     * @return int
+     */
+    public function getNewChapterPriority(Chapter $chapter)
+    {
+        $lastPage = $chapter->pages('DESC')->first();
+        return $lastPage !== null ? $lastPage->priority + 1 : 0;
+    }
+
+    /**
+     * Find a suitable slug for an entity.
+     * @param string $type
+     * @param string $name
+     * @param bool|integer $currentId
+     * @param bool|integer $bookId Only pass if type is not a book
+     * @return string
+     */
+    public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
+    {
+        $slug = $this->nameToSlug($name);
+        while ($this->slugExists($type, $slug, $currentId, $bookId)) {
+            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+        }
+        return $slug;
+    }
+
+    /**
+     * Check if a slug already exists in the database.
+     * @param string $type
+     * @param string $slug
+     * @param bool|integer $currentId
+     * @param bool|integer $bookId
+     * @return bool
+     */
+    protected function slugExists($type, $slug, $currentId = false, $bookId = false)
+    {
+        $query = $this->getEntity($type)->where('slug', '=', $slug);
+        if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
+            $query = $query->where('book_id', '=', $bookId);
+        }
+        if ($currentId) $query = $query->where('id', '!=', $currentId);
+        return $query->count() > 0;
+    }
+
     /**
      * Updates entity restrictions from a request
      * @param $request
@@ -260,6 +587,81 @@ class EntityRepo
         return $query;
     }
 
+    /**
+     * Create a new entity from request input.
+     * Used for books and chapters.
+     * @param string $type
+     * @param array $input
+     * @param bool|Book $book
+     * @return Entity
+     */
+    public function createFromInput($type, $input = [], $book = false)
+    {
+        $isChapter = strtolower($type) === 'chapter';
+        $entity = $this->getEntity($type)->newInstance($input);
+        $entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
+        $entity->created_by = user()->id;
+        $entity->updated_by = user()->id;
+        $isChapter ? $book->chapters()->save($entity) : $entity->save();
+        $this->permissionService->buildJointPermissionsForEntity($entity);
+        return $entity;
+    }
+
+    /**
+     * Update entity details from request input.
+     * Use for books and chapters
+     * @param string $type
+     * @param Entity $entityModel
+     * @param array $input
+     * @return Entity
+     */
+    public function updateFromInput($type, Entity $entityModel, $input = [])
+    {
+        if ($entityModel->name !== $input['name']) {
+            $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
+        }
+        $entityModel->fill($input);
+        $entityModel->updated_by = user()->id;
+        $entityModel->save();
+        $this->permissionService->buildJointPermissionsForEntity($entityModel);
+        return $entityModel;
+    }
+
+    /**
+     * Change the book that an entity belongs to.
+     * @param string $type
+     * @param integer $newBookId
+     * @param Entity $entity
+     * @param bool $rebuildPermissions
+     * @return Entity
+     */
+    public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
+    {
+        $entity->book_id = $newBookId;
+        // Update related activity
+        foreach ($entity->activity as $activity) {
+            $activity->book_id = $newBookId;
+            $activity->save();
+        }
+        $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
+        $entity->save();
+
+        // Update all child pages if a chapter
+        if (strtolower($type) === 'chapter') {
+            foreach ($entity->pages as $page) {
+                $this->changeBook('page', $newBookId, $page, false);
+            }
+        }
+
+        // Update permissions if applicable
+        if ($rebuildPermissions) {
+            $entity->load('book');
+            $this->permissionService->buildJointPermissionsForEntity($entity->book);
+        }
+
+        return $entity;
+    }
+
     /**
      * Alias method to update the book jointPermissions in the PermissionService.
      * @param Collection $collection collection on entities
@@ -282,6 +684,472 @@ class EntityRepo
         return $slug;
     }
 
+    /**
+     * Publish a draft page to make it a normal page.
+     * Sets the slug and updates the content.
+     * @param Page $draftPage
+     * @param array $input
+     * @return Page
+     */
+    public function publishPageDraft(Page $draftPage, array $input)
+    {
+        $draftPage->fill($input);
+
+        // Save page tags if present
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
+        }
+
+        $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
+        $draftPage->html = $this->formatHtml($input['html']);
+        $draftPage->text = strip_tags($draftPage->html);
+        $draftPage->draft = false;
+
+        $draftPage->save();
+        $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
+
+        return $draftPage;
+    }
+
+    /**
+     * Saves a page revision into the system.
+     * @param Page $page
+     * @param null|string $summary
+     * @return PageRevision
+     */
+    public function savePageRevision(Page $page, $summary = null)
+    {
+        $revision = $this->pageRevision->newInstance($page->toArray());
+        if (setting('app-editor') !== 'markdown') $revision->markdown = '';
+        $revision->page_id = $page->id;
+        $revision->slug = $page->slug;
+        $revision->book_slug = $page->book->slug;
+        $revision->created_by = user()->id;
+        $revision->created_at = $page->updated_at;
+        $revision->type = 'version';
+        $revision->summary = $summary;
+        $revision->save();
+
+        // Clear old revisions
+        if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
+            $this->pageRevision->where('page_id', '=', $page->id)
+                ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
+        }
+
+        return $revision;
+    }
+
+    /**
+     * Formats a page's html to be tagged correctly
+     * within the system.
+     * @param string $htmlText
+     * @return string
+     */
+    protected function formatHtml($htmlText)
+    {
+        if ($htmlText == '') return $htmlText;
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
+
+        $container = $doc->documentElement;
+        $body = $container->childNodes->item(0);
+        $childNodes = $body->childNodes;
+
+        // Ensure no duplicate ids are used
+        $idArray = [];
+
+        foreach ($childNodes as $index => $childNode) {
+            /** @var \DOMElement $childNode */
+            if (get_class($childNode) !== 'DOMElement') continue;
+
+            // Overwrite id if not a BookStack custom id
+            if ($childNode->hasAttribute('id')) {
+                $id = $childNode->getAttribute('id');
+                if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
+                    $idArray[] = $id;
+                    continue;
+                };
+            }
+
+            // Create an unique id for the element
+            // Uses the content as a basis to ensure output is the same every time
+            // the same content is passed through.
+            $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
+            $newId = urlencode($contentId);
+            $loopIndex = 0;
+            while (in_array($newId, $idArray)) {
+                $newId = urlencode($contentId . '-' . $loopIndex);
+                $loopIndex++;
+            }
+
+            $childNode->setAttribute('id', $newId);
+            $idArray[] = $newId;
+        }
+
+        // Generate inner html as a string
+        $html = '';
+        foreach ($childNodes as $childNode) {
+            $html .= $doc->saveHTML($childNode);
+        }
+
+        return $html;
+    }
+
+    /**
+     * Get a new draft page instance.
+     * @param Book $book
+     * @param Chapter|bool $chapter
+     * @return Page
+     */
+    public function getDraftPage(Book $book, $chapter = false)
+    {
+        $page = $this->page->newInstance();
+        $page->name = trans('entities.pages_initial_name');
+        $page->created_by = user()->id;
+        $page->updated_by = user()->id;
+        $page->draft = true;
+
+        if ($chapter) $page->chapter_id = $chapter->id;
+
+        $book->pages()->save($page);
+        $this->permissionService->buildJointPermissionsForEntity($page);
+        return $page;
+    }
+
+    /**
+     * Search for image usage within page content.
+     * @param $imageString
+     * @return mixed
+     */
+    public function searchForImage($imageString)
+    {
+        $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
+        foreach ($pages as $page) {
+            $page->url = $page->getUrl();
+            $page->html = '';
+            $page->text = '';
+        }
+        return count($pages) > 0 ? $pages : false;
+    }
+
+    /**
+     * Parse the headers on the page to get a navigation menu
+     * @param Page $page
+     * @return Collection
+     */
+    public function getPageNav(Page $page)
+    {
+        if ($page->html == '') return null;
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
+        $xPath = new DOMXPath($doc);
+        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
+
+        if (is_null($headers)) return null;
+
+        $tree = collect([]);
+        foreach ($headers as $header) {
+            $text = $header->nodeValue;
+            $tree->push([
+                'nodeName' => strtolower($header->nodeName),
+                'level' => intval(str_replace('h', '', $header->nodeName)),
+                'link' => '#' . $header->getAttribute('id'),
+                'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
+            ]);
+        }
+
+        // Normalise headers if only smaller headers have been used
+        if (count($tree) > 0) {
+            $minLevel = $tree->pluck('level')->min();
+            $tree = $tree->map(function($header) use ($minLevel) {
+                $header['level'] -= ($minLevel - 2);
+                return $header;
+            });
+        }
+        return $tree;
+    }
+
+    /**
+     * Updates a page with any fillable data and saves it into the database.
+     * @param Page $page
+     * @param int $book_id
+     * @param array $input
+     * @return Page
+     */
+    public function updatePage(Page $page, $book_id, $input)
+    {
+        // Hold the old details to compare later
+        $oldHtml = $page->html;
+        $oldName = $page->name;
+
+        // Prevent slug being updated if no name change
+        if ($page->name !== $input['name']) {
+            $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+        }
+
+        // Save page tags if present
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($page, $input['tags']);
+        }
+
+        // Update with new details
+        $userId = user()->id;
+        $page->fill($input);
+        $page->html = $this->formatHtml($input['html']);
+        $page->text = strip_tags($page->html);
+        if (setting('app-editor') !== 'markdown') $page->markdown = '';
+        $page->updated_by = $userId;
+        $page->save();
+
+        // Remove all update drafts for this user & page.
+        $this->userUpdatePageDraftsQuery($page, $userId)->delete();
+
+        // Save a revision after updating
+        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
+            $this->savePageRevision($page, $input['summary']);
+        }
+
+        return $page;
+    }
+
+    /**
+     * The base query for getting user update drafts.
+     * @param Page $page
+     * @param $userId
+     * @return mixed
+     */
+    protected function userUpdatePageDraftsQuery(Page $page, $userId)
+    {
+        return $this->pageRevision->where('created_by', '=', $userId)
+            ->where('type', 'update_draft')
+            ->where('page_id', '=', $page->id)
+            ->orderBy('created_at', 'desc');
+    }
+
+    /**
+     * Checks whether a user has a draft version of a particular page or not.
+     * @param Page $page
+     * @param $userId
+     * @return bool
+     */
+    public function hasUserGotPageDraft(Page $page, $userId)
+    {
+        return $this->userUpdatePageDraftsQuery($page, $userId)->count() > 0;
+    }
+
+    /**
+     * Get the latest updated draft revision for a particular page and user.
+     * @param Page $page
+     * @param $userId
+     * @return mixed
+     */
+    public function getUserPageDraft(Page $page, $userId)
+    {
+        return $this->userUpdatePageDraftsQuery($page, $userId)->first();
+    }
+
+    /**
+     * Get the notification message that informs the user that they are editing a draft page.
+     * @param PageRevision $draft
+     * @return string
+     */
+    public function getUserPageDraftMessage(PageRevision $draft)
+    {
+        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
+        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
+        return $message . "\n" . trans('entities.pages_draft_edited_notification');
+    }
+
+    /**
+     * Check if a page is being actively editing.
+     * Checks for edits since last page updated.
+     * Passing in a minuted range will check for edits
+     * within the last x minutes.
+     * @param Page $page
+     * @param null $minRange
+     * @return bool
+     */
+    public function isPageEditingActive(Page $page, $minRange = null)
+    {
+        $draftSearch = $this->activePageEditingQuery($page, $minRange);
+        return $draftSearch->count() > 0;
+    }
+
+    /**
+     * A query to check for active update drafts on a particular page.
+     * @param Page $page
+     * @param null $minRange
+     * @return mixed
+     */
+    protected function activePageEditingQuery(Page $page, $minRange = null)
+    {
+        $query = $this->pageRevision->where('type', '=', 'update_draft')
+            ->where('page_id', '=', $page->id)
+            ->where('updated_at', '>', $page->updated_at)
+            ->where('created_by', '!=', user()->id)
+            ->with('createdBy');
+
+        if ($minRange !== null) {
+            $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+        }
+
+        return $query;
+    }
+
+    /**
+     * Restores a revision's content back into a page.
+     * @param Page $page
+     * @param Book $book
+     * @param  int $revisionId
+     * @return Page
+     */
+    public function restorePageRevision(Page $page, Book $book, $revisionId)
+    {
+        $this->savePageRevision($page);
+        $revision = $this->getById('page_revision', $revisionId);
+        $page->fill($revision->toArray());
+        $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
+        $page->text = strip_tags($page->html);
+        $page->updated_by = user()->id;
+        $page->save();
+        return $page;
+    }
+
+
+    /**
+     * Save a page update draft.
+     * @param Page $page
+     * @param array $data
+     * @return PageRevision|Page
+     */
+    public function updatePageDraft(Page $page, $data = [])
+    {
+        // If the page itself is a draft simply update that
+        if ($page->draft) {
+            $page->fill($data);
+            if (isset($data['html'])) {
+                $page->text = strip_tags($data['html']);
+            }
+            $page->save();
+            return $page;
+        }
+
+        // Otherwise save the data to a revision
+        $userId = user()->id;
+        $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
+
+        if ($drafts->count() > 0) {
+            $draft = $drafts->first();
+        } else {
+            $draft = $this->pageRevision->newInstance();
+            $draft->page_id = $page->id;
+            $draft->slug = $page->slug;
+            $draft->book_slug = $page->book->slug;
+            $draft->created_by = $userId;
+            $draft->type = 'update_draft';
+        }
+
+        $draft->fill($data);
+        if (setting('app-editor') !== 'markdown') $draft->markdown = '';
+
+        $draft->save();
+        return $draft;
+    }
+
+    /**
+     * Get a notification message concerning the editing activity on a particular page.
+     * @param Page $page
+     * @param null $minRange
+     * @return string
+     */
+    public function getPageEditingActiveMessage(Page $page, $minRange = null)
+    {
+        $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+
+        $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+        $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
+        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+    }
+
+    /**
+     * Change the page's parent to the given entity.
+     * @param Page $page
+     * @param Entity $parent
+     */
+    public function changePageParent(Page $page, Entity $parent)
+    {
+        $book = $parent->isA('book') ? $parent : $parent->book;
+        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
+        $page->save();
+        if ($page->book->id !== $book->id) {
+            $page = $this->changeBook('page', $book->id, $page);
+        }
+        $page->load('book');
+        $this->permissionService->buildJointPermissionsForEntity($book);
+    }
+
+    /**
+     * Destroy the provided book and all its child entities.
+     * @param Book $book
+     */
+    public function destroyBook(Book $book)
+    {
+        foreach ($book->pages as $page) {
+            $this->destroyPage($page);
+        }
+        foreach ($book->chapters as $chapter) {
+            $this->destroyChapter($chapter);
+        }
+        \Activity::removeEntity($book);
+        $book->views()->delete();
+        $book->permissions()->delete();
+        $this->permissionService->deleteJointPermissionsForEntity($book);
+        $book->delete();
+    }
+
+    /**
+     * Destroy a chapter and its relations.
+     * @param Chapter $chapter
+     */
+    public function destroyChapter(Chapter $chapter)
+    {
+        if (count($chapter->pages) > 0) {
+            foreach ($chapter->pages as $page) {
+                $page->chapter_id = 0;
+                $page->save();
+            }
+        }
+        \Activity::removeEntity($chapter);
+        $chapter->views()->delete();
+        $chapter->permissions()->delete();
+        $this->permissionService->deleteJointPermissionsForEntity($chapter);
+        $chapter->delete();
+    }
+
+    /**
+     * Destroy a given page along with its dependencies.
+     * @param Page $page
+     */
+    public function destroyPage(Page $page)
+    {
+        \Activity::removeEntity($page);
+        $page->views()->delete();
+        $page->tags()->delete();
+        $page->revisions()->delete();
+        $page->permissions()->delete();
+        $this->permissionService->deleteJointPermissionsForEntity($page);
+
+        // Delete Attached Files
+        $attachmentService = app(AttachmentService::class);
+        foreach ($page->attachments as $attachment) {
+            $attachmentService->deleteFile($attachment);
+        }
+
+        $page->delete();
+    }
+
 }
 
 
diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php
deleted file mode 100644
index e6d713f77..000000000
--- a/app/Repos/PageRepo.php
+++ /dev/null
@@ -1,666 +0,0 @@
-<?php namespace BookStack\Repos;
-
-use Activity;
-use BookStack\Book;
-use BookStack\Chapter;
-use BookStack\Entity;
-use BookStack\Exceptions\NotFoundException;
-use BookStack\Services\AttachmentService;
-use Carbon\Carbon;
-use DOMDocument;
-use DOMXPath;
-use Illuminate\Support\Str;
-use BookStack\Page;
-use BookStack\PageRevision;
-
-class PageRepo extends EntityRepo
-{
-
-    protected $pageRevision;
-    protected $tagRepo;
-
-    /**
-     * PageRepo constructor.
-     * @param PageRevision $pageRevision
-     * @param TagRepo $tagRepo
-     */
-    public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
-    {
-        $this->pageRevision = $pageRevision;
-        $this->tagRepo = $tagRepo;
-        parent::__construct();
-    }
-
-    /**
-     * Base query for getting pages, Takes restrictions into account.
-     * @param bool $allowDrafts
-     * @return mixed
-     */
-    private function pageQuery($allowDrafts = false)
-    {
-        $query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
-        if (!$allowDrafts) {
-            $query = $query->where('draft', '=', false);
-        }
-        return $query;
-    }
-
-    /**
-     * Get a page via a specific ID.
-     * @param $id
-     * @param bool $allowDrafts
-     * @return Page
-     */
-    public function getById($id, $allowDrafts = false)
-    {
-        return $this->pageQuery($allowDrafts)->findOrFail($id);
-    }
-
-    /**
-     * Get a page identified by the given slug.
-     * @param $slug
-     * @param $bookId
-     * @return Page
-     * @throws NotFoundException
-     */
-    public function getBySlug($slug, $bookId)
-    {
-        $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
-        if ($page === null) throw new NotFoundException('Page not found');
-        return $page;
-    }
-
-    /**
-     * Search through page revisions and retrieve
-     * the last page in the current book that
-     * has a slug equal to the one given.
-     * @param $pageSlug
-     * @param $bookSlug
-     * @return null | Page
-     */
-    public function findPageUsingOldSlug($pageSlug, $bookSlug)
-    {
-        $revision = $this->pageRevision->where('slug', '=', $pageSlug)
-            ->whereHas('page', function ($query) {
-                $this->permissionService->enforcePageRestrictions($query);
-            })
-            ->where('type', '=', 'version')
-            ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
-            ->with('page')->first();
-        return $revision !== null ? $revision->page : null;
-    }
-
-    /**
-     * Get a new Page instance from the given input.
-     * @param $input
-     * @return Page
-     */
-    public function newFromInput($input)
-    {
-        $page = $this->page->fill($input);
-        return $page;
-    }
-
-    /**
-     * Count the pages with a particular slug within a book.
-     * @param $slug
-     * @param $bookId
-     * @return mixed
-     */
-    public function countBySlug($slug, $bookId)
-    {
-        return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
-    }
-
-    /**
-     * Publish a draft page to make it a normal page.
-     * Sets the slug and updates the content.
-     * @param Page $draftPage
-     * @param array $input
-     * @return Page
-     */
-    public function publishDraft(Page $draftPage, array $input)
-    {
-        $draftPage->fill($input);
-
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
-        }
-
-        $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
-        $draftPage->html = $this->formatHtml($input['html']);
-        $draftPage->text = strip_tags($draftPage->html);
-        $draftPage->draft = false;
-
-        $draftPage->save();
-        $this->saveRevision($draftPage, 'Initial Publish');
-        
-        return $draftPage;
-    }
-
-    /**
-     * Get a new draft page instance.
-     * @param Book $book
-     * @param Chapter|bool $chapter
-     * @return static
-     */
-    public function getDraftPage(Book $book, $chapter = false)
-    {
-        $page = $this->page->newInstance();
-        $page->name = 'New Page';
-        $page->created_by = user()->id;
-        $page->updated_by = user()->id;
-        $page->draft = true;
-
-        if ($chapter) $page->chapter_id = $chapter->id;
-
-        $book->pages()->save($page);
-        $this->permissionService->buildJointPermissionsForEntity($page);
-        return $page;
-    }
-
-    /**
-     * Parse te headers on the page to get a navigation menu
-     * @param Page $page
-     * @return array
-     */
-    public function getPageNav(Page $page)
-    {
-        if ($page->html == '') return null;
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
-        $xPath = new DOMXPath($doc);
-        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
-
-        if (is_null($headers)) return null;
-
-        $tree = [];
-        foreach ($headers as $header) {
-            $text = $header->nodeValue;
-            $tree[] = [
-                'nodeName' => strtolower($header->nodeName),
-                'level' => intval(str_replace('h', '', $header->nodeName)),
-                'link' => '#' . $header->getAttribute('id'),
-                'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
-            ];
-        }
-        return $tree;
-    }
-
-    /**
-     * Formats a page's html to be tagged correctly
-     * within the system.
-     * @param string $htmlText
-     * @return string
-     */
-    protected function formatHtml($htmlText)
-    {
-        if ($htmlText == '') return $htmlText;
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
-
-        $container = $doc->documentElement;
-        $body = $container->childNodes->item(0);
-        $childNodes = $body->childNodes;
-
-        // Ensure no duplicate ids are used
-        $idArray = [];
-
-        foreach ($childNodes as $index => $childNode) {
-            /** @var \DOMElement $childNode */
-            if (get_class($childNode) !== 'DOMElement') continue;
-
-            // Overwrite id if not a BookStack custom id
-            if ($childNode->hasAttribute('id')) {
-                $id = $childNode->getAttribute('id');
-                if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
-                    $idArray[] = $id;
-                    continue;
-                };
-            }
-
-            // Create an unique id for the element
-            // Uses the content as a basis to ensure output is the same every time
-            // the same content is passed through.
-            $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
-            $newId = urlencode($contentId);
-            $loopIndex = 0;
-            while (in_array($newId, $idArray)) {
-                $newId = urlencode($contentId . '-' . $loopIndex);
-                $loopIndex++;
-            }
-
-            $childNode->setAttribute('id', $newId);
-            $idArray[] = $newId;
-        }
-
-        // Generate inner html as a string
-        $html = '';
-        foreach ($childNodes as $childNode) {
-            $html .= $doc->saveHTML($childNode);
-        }
-
-        return $html;
-    }
-
-
-    /**
-     * Gets pages by a search term.
-     * Highlights page content for showing in results.
-     * @param string $term
-     * @param array $whereTerms
-     * @param int $count
-     * @param array $paginationAppends
-     * @return mixed
-     */
-    public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
-    {
-        $terms = $this->prepareSearchTerms($term);
-        $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
-        $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
-        $pages = $pageQuery->paginate($count)->appends($paginationAppends);
-
-        // Add highlights to page text.
-        $words = join('|', explode(' ', preg_quote(trim($term), '/')));
-        //lookahead/behind assertions ensures cut between words
-        $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
-
-        foreach ($pages as $page) {
-            preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
-            //delimiter between occurrences
-            $results = [];
-            foreach ($matches as $line) {
-                $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
-            }
-            $matchLimit = 6;
-            if (count($results) > $matchLimit) {
-                $results = array_slice($results, 0, $matchLimit);
-            }
-            $result = join('... ', $results);
-
-            //highlight
-            $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
-            if (strlen($result) < 5) {
-                $result = $page->getExcerpt(80);
-            }
-            $page->searchSnippet = $result;
-        }
-        return $pages;
-    }
-
-    /**
-     * Search for image usage.
-     * @param $imageString
-     * @return mixed
-     */
-    public function searchForImage($imageString)
-    {
-        $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
-        foreach ($pages as $page) {
-            $page->url = $page->getUrl();
-            $page->html = '';
-            $page->text = '';
-        }
-        return count($pages) > 0 ? $pages : false;
-    }
-
-    /**
-     * Updates a page with any fillable data and saves it into the database.
-     * @param Page $page
-     * @param int $book_id
-     * @param string $input
-     * @return Page
-     */
-    public function updatePage(Page $page, $book_id, $input)
-    {
-        // Hold the old details to compare later
-        $oldHtml = $page->html;
-        $oldName = $page->name;
-
-        // Prevent slug being updated if no name change
-        if ($page->name !== $input['name']) {
-            $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
-        }
-
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($page, $input['tags']);
-        }
-
-        // Update with new details
-        $userId = user()->id;
-        $page->fill($input);
-        $page->html = $this->formatHtml($input['html']);
-        $page->text = strip_tags($page->html);
-        if (setting('app-editor') !== 'markdown') $page->markdown = '';
-        $page->updated_by = $userId;
-        $page->save();
-
-        // Remove all update drafts for this user & page.
-        $this->userUpdateDraftsQuery($page, $userId)->delete();
-
-        // Save a revision after updating
-        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
-            $this->saveRevision($page, $input['summary']);
-        }
-
-        return $page;
-    }
-
-    /**
-     * Restores a revision's content back into a page.
-     * @param Page $page
-     * @param Book $book
-     * @param  int $revisionId
-     * @return Page
-     */
-    public function restoreRevision(Page $page, Book $book, $revisionId)
-    {
-        $this->saveRevision($page);
-        $revision = $this->getRevisionById($revisionId);
-        $page->fill($revision->toArray());
-        $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
-        $page->text = strip_tags($page->html);
-        $page->updated_by = user()->id;
-        $page->save();
-        return $page;
-    }
-
-    /**
-     * Saves a page revision into the system.
-     * @param Page $page
-     * @param null|string $summary
-     * @return $this
-     */
-    public function saveRevision(Page $page, $summary = null)
-    {
-        $revision = $this->pageRevision->newInstance($page->toArray());
-        if (setting('app-editor') !== 'markdown') $revision->markdown = '';
-        $revision->page_id = $page->id;
-        $revision->slug = $page->slug;
-        $revision->book_slug = $page->book->slug;
-        $revision->created_by = user()->id;
-        $revision->created_at = $page->updated_at;
-        $revision->type = 'version';
-        $revision->summary = $summary;
-        $revision->save();
-
-        // Clear old revisions
-        if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
-            $this->pageRevision->where('page_id', '=', $page->id)
-                ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
-        }
-
-        return $revision;
-    }
-
-    /**
-     * Save a page update draft.
-     * @param Page $page
-     * @param array $data
-     * @return PageRevision
-     */
-    public function saveUpdateDraft(Page $page, $data = [])
-    {
-        $userId = user()->id;
-        $drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
-
-        if ($drafts->count() > 0) {
-            $draft = $drafts->first();
-        } else {
-            $draft = $this->pageRevision->newInstance();
-            $draft->page_id = $page->id;
-            $draft->slug = $page->slug;
-            $draft->book_slug = $page->book->slug;
-            $draft->created_by = $userId;
-            $draft->type = 'update_draft';
-        }
-
-        $draft->fill($data);
-        if (setting('app-editor') !== 'markdown') $draft->markdown = '';
-
-        $draft->save();
-        return $draft;
-    }
-
-    /**
-     * Update a draft page.
-     * @param Page $page
-     * @param array $data
-     * @return Page
-     */
-    public function updateDraftPage(Page $page, $data = [])
-    {
-        $page->fill($data);
-
-        if (isset($data['html'])) {
-            $page->text = strip_tags($data['html']);
-        }
-
-        $page->save();
-        return $page;
-    }
-
-    /**
-     * The base query for getting user update drafts.
-     * @param Page $page
-     * @param $userId
-     * @return mixed
-     */
-    private function userUpdateDraftsQuery(Page $page, $userId)
-    {
-        return $this->pageRevision->where('created_by', '=', $userId)
-            ->where('type', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->orderBy('created_at', 'desc');
-    }
-
-    /**
-     * Checks whether a user has a draft version of a particular page or not.
-     * @param Page $page
-     * @param $userId
-     * @return bool
-     */
-    public function hasUserGotPageDraft(Page $page, $userId)
-    {
-        return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
-    }
-
-    /**
-     * Get the latest updated draft revision for a particular page and user.
-     * @param Page $page
-     * @param $userId
-     * @return mixed
-     */
-    public function getUserPageDraft(Page $page, $userId)
-    {
-        return $this->userUpdateDraftsQuery($page, $userId)->first();
-    }
-
-    /**
-     * Get the notification message that informs the user that they are editing a draft page.
-     * @param PageRevision $draft
-     * @return string
-     */
-    public function getUserPageDraftMessage(PageRevision $draft)
-    {
-        $message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
-        if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
-            $message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
-        }
-        return $message;
-    }
-
-    /**
-     * Check if a page is being actively editing.
-     * Checks for edits since last page updated.
-     * Passing in a minuted range will check for edits
-     * within the last x minutes.
-     * @param Page $page
-     * @param null $minRange
-     * @return bool
-     */
-    public function isPageEditingActive(Page $page, $minRange = null)
-    {
-        $draftSearch = $this->activePageEditingQuery($page, $minRange);
-        return $draftSearch->count() > 0;
-    }
-
-    /**
-     * Get a notification message concerning the editing activity on
-     * a particular page.
-     * @param Page $page
-     * @param null $minRange
-     * @return string
-     */
-    public function getPageEditingActiveMessage(Page $page, $minRange = null)
-    {
-        $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
-        $userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
-        $timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
-        $message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
-        return sprintf($message, $userMessage, $timeMessage);
-    }
-
-    /**
-     * A query to check for active update drafts on a particular page.
-     * @param Page $page
-     * @param null $minRange
-     * @return mixed
-     */
-    private function activePageEditingQuery(Page $page, $minRange = null)
-    {
-        $query = $this->pageRevision->where('type', '=', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->where('updated_at', '>', $page->updated_at)
-            ->where('created_by', '!=', user()->id)
-            ->with('createdBy');
-
-        if ($minRange !== null) {
-            $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
-        }
-
-        return $query;
-    }
-
-    /**
-     * Gets a single revision via it's id.
-     * @param $id
-     * @return PageRevision
-     */
-    public function getRevisionById($id)
-    {
-        return $this->pageRevision->findOrFail($id);
-    }
-
-    /**
-     * Checks if a slug exists within a book already.
-     * @param            $slug
-     * @param            $bookId
-     * @param bool|false $currentId
-     * @return bool
-     */
-    public function doesSlugExist($slug, $bookId, $currentId = false)
-    {
-        $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
-        if ($currentId) $query = $query->where('id', '!=', $currentId);
-        return $query->count() > 0;
-    }
-
-    /**
-     * Changes the related book for the specified page.
-     * Changes the book id of any relations to the page that store the book id.
-     * @param int $bookId
-     * @param Page $page
-     * @return Page
-     */
-    public function changeBook($bookId, Page $page)
-    {
-        $page->book_id = $bookId;
-        foreach ($page->activity as $activity) {
-            $activity->book_id = $bookId;
-            $activity->save();
-        }
-        $page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
-        $page->save();
-        return $page;
-    }
-
-
-    /**
-     * Change the page's parent to the given entity.
-     * @param Page $page
-     * @param Entity $parent
-     */
-    public function changePageParent(Page $page, Entity $parent)
-    {
-        $book = $parent->isA('book') ? $parent : $parent->book;
-        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
-        $page->save();
-        $page = $this->changeBook($book->id, $page);
-        $page->load('book');
-        $this->permissionService->buildJointPermissionsForEntity($book);
-    }
-
-    /**
-     * Gets a suitable slug for the resource
-     * @param string $name
-     * @param int $bookId
-     * @param bool|false $currentId
-     * @return string
-     */
-    public function findSuitableSlug($name, $bookId, $currentId = false)
-    {
-        $slug = $this->nameToSlug($name);
-        while ($this->doesSlugExist($slug, $bookId, $currentId)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
-        }
-        return $slug;
-    }
-
-    /**
-     * Destroy a given page along with its dependencies.
-     * @param $page
-     */
-    public function destroy(Page $page)
-    {
-        Activity::removeEntity($page);
-        $page->views()->delete();
-        $page->tags()->delete();
-        $page->revisions()->delete();
-        $page->permissions()->delete();
-        $this->permissionService->deleteJointPermissionsForEntity($page);
-
-        // Delete AttachedFiles
-        $attachmentService = app(AttachmentService::class);
-        foreach ($page->attachments as $attachment) {
-            $attachmentService->deleteFile($attachment);
-        }
-
-        $page->delete();
-    }
-
-    /**
-     * Get the latest pages added to the system.
-     * @param $count
-     * @return mixed
-     */
-    public function getRecentlyCreatedPaginated($count = 20)
-    {
-        return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
-    }
-
-    /**
-     * Get the latest pages added to the system.
-     * @param $count
-     * @return mixed
-     */
-    public function getRecentlyUpdatedPaginated($count = 20)
-    {
-        return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
-    }
-
-}
diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php
index 24497c911..e1c6d87b1 100644
--- a/app/Repos/PermissionsRepo.php
+++ b/app/Repos/PermissionsRepo.php
@@ -133,9 +133,9 @@ class PermissionsRepo
 
         // Prevent deleting admin role or default registration role.
         if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
-            throw new PermissionsException('This role is a system role and cannot be deleted');
+            throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
         } else if ($role->id == setting('registration-role')) {
-            throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
+            throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
         }
 
         if ($migrateRoleId) {
diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php
index 6d0857f8b..c6350db1a 100644
--- a/app/Repos/TagRepo.php
+++ b/app/Repos/TagRepo.php
@@ -38,7 +38,7 @@ class TagRepo
     {
         $entityInstance = $this->entity->getEntityInstance($entityType);
         $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
-        $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
+        $searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
         return $searchQuery->first();
     }
 
@@ -121,7 +121,7 @@ class TagRepo
     /**
      * Create a new Tag instance from user input.
      * @param $input
-     * @return static
+     * @return Tag
      */
     protected function newInstanceFromInput($input)
     {
diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php
index ab3716fca..c3546a442 100644
--- a/app/Repos/UserRepo.php
+++ b/app/Repos/UserRepo.php
@@ -3,7 +3,6 @@
 use BookStack\Role;
 use BookStack\User;
 use Exception;
-use Setting;
 
 class UserRepo
 {
@@ -169,13 +168,13 @@ class UserRepo
     public function getRecentlyCreated(User $user, $count = 20)
     {
         return [
-            'pages'    => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
+            'pages'    => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
                 $query->where('created_by', '=', $user->id);
             }),
-            'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
+            'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
                 $query->where('created_by', '=', $user->id);
             }),
-            'books'    => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
+            'books'    => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
                 $query->where('created_by', '=', $user->id);
             })
         ];
diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php
index e41036238..2368ba10a 100644
--- a/app/Services/ActivityService.php
+++ b/app/Services/ActivityService.php
@@ -114,7 +114,7 @@ class ActivityService
         
         $activity = $this->permissionService
             ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
-            ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
+            ->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
 
         return $this->filterSimilar($activity);
     }
diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php
index e0ee3a04d..592d67e63 100644
--- a/app/Services/AttachmentService.php
+++ b/app/Services/AttachmentService.php
@@ -193,7 +193,7 @@ class AttachmentService extends UploadService
         try {
             $storage->put($attachmentStoragePath, $attachmentData);
         } catch (Exception $e) {
-            throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
+            throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
         }
         return $attachmentPath;
     }
diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php
index d4ec1e976..8eb52708c 100644
--- a/app/Services/EmailConfirmationService.php
+++ b/app/Services/EmailConfirmationService.php
@@ -33,7 +33,7 @@ class EmailConfirmationService
     public function sendConfirmation(User $user)
     {
         if ($user->email_confirmed) {
-            throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
+            throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
         }
 
         $this->deleteConfirmationsByUser($user);
@@ -63,7 +63,7 @@ class EmailConfirmationService
      * Gets an email confirmation by looking up the token,
      * Ensures the token has not expired.
      * @param string $token
-     * @return EmailConfirmation
+     * @return array|null|\stdClass
      * @throws UserRegistrationException
      */
     public function getEmailConfirmationFromToken($token)
@@ -72,14 +72,14 @@ class EmailConfirmationService
 
         // If not found show error
         if ($emailConfirmation === null) {
-            throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
+            throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
         }
 
         // If more than a day old
         if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
             $user = $this->users->getById($emailConfirmation->user_id);
             $this->sendConfirmation($user);
-            throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
+            throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
         }
 
         $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php
index 14084d320..50ba75c17 100644
--- a/app/Services/ExportService.php
+++ b/app/Services/ExportService.php
@@ -1,6 +1,5 @@
 <?php namespace BookStack\Services;
 
-
 use BookStack\Page;
 
 class ExportService
@@ -28,8 +27,13 @@ class ExportService
     {
         $cssContent = file_get_contents(public_path('/css/export-styles.css'));
         $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
+        $useWKHTML = config('snappy.pdf.binary') !== false;
         $containedHtml = $this->containHtml($pageHtml);
-        $pdf = \PDF::loadHTML($containedHtml);
+        if ($useWKHTML) {
+            $pdf = \SnappyPDF::loadHTML($containedHtml);
+        } else {
+            $pdf = \PDF::loadHTML($containedHtml);
+        }
         return $pdf->output();
     }
 
diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php
index dfe2cf453..e34b3fb2b 100644
--- a/app/Services/ImageService.php
+++ b/app/Services/ImageService.php
@@ -59,7 +59,7 @@ class ImageService extends UploadService
     {
         $imageName = $imageName ? $imageName : basename($url);
         $imageData = file_get_contents($url);
-        if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
+        if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
         return $this->saveNew($imageName, $imageData, $type);
     }
 
@@ -93,7 +93,7 @@ class ImageService extends UploadService
             $storage->put($fullPath, $imageData);
             $storage->setVisibility($fullPath, 'public');
         } catch (Exception $e) {
-            throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
+            throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
         }
 
         if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
@@ -160,7 +160,7 @@ class ImageService extends UploadService
             $thumb = $this->imageTool->make($storage->get($imagePath));
         } catch (Exception $e) {
             if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
-                throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
+                throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
             } else {
                 throw $e;
             }
diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php
index b7f101ad2..40b24f141 100644
--- a/app/Services/LdapService.php
+++ b/app/Services/LdapService.php
@@ -94,7 +94,7 @@ class LdapService
             $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
         }
 
-        if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
+        if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
     }
 
     /**
@@ -109,7 +109,7 @@ class LdapService
 
         // Check LDAP extension in installed
         if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
-            throw new LdapException('LDAP PHP extension not installed');
+            throw new LdapException(trans('errors.ldap_extension_not_installed'));
         }
 
         // Get port from server string if specified.
@@ -117,7 +117,7 @@ class LdapService
         $ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
 
         if ($ldapConnection === false) {
-            throw new LdapException('Cannot connect to ldap server, Initial connection failed');
+            throw new LdapException(trans('errors.ldap_cannot_connect'));
         }
 
         // Set any required options
diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php
index bb78f0b0a..467bf95da 100644
--- a/app/Services/PermissionService.php
+++ b/app/Services/PermissionService.php
@@ -8,8 +8,9 @@ use BookStack\Ownable;
 use BookStack\Page;
 use BookStack\Role;
 use BookStack\User;
+use Illuminate\Database\Connection;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Log;
 
 class PermissionService
 {
@@ -23,6 +24,8 @@ class PermissionService
     public $chapter;
     public $page;
 
+    protected $db;
+
     protected $jointPermission;
     protected $role;
 
@@ -31,18 +34,21 @@ class PermissionService
     /**
      * PermissionService constructor.
      * @param JointPermission $jointPermission
+     * @param Connection $db
      * @param Book $book
      * @param Chapter $chapter
      * @param Page $page
      * @param Role $role
      */
-    public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
+    public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
     {
+        $this->db = $db;
         $this->jointPermission = $jointPermission;
         $this->role = $role;
         $this->book = $book;
         $this->chapter = $chapter;
         $this->page = $page;
+        // TODO - Update so admin still goes through filters
     }
 
     /**
@@ -302,6 +308,10 @@ class PermissionService
         $explodedAction = explode('-', $action);
         $restrictionAction = end($explodedAction);
 
+        if ($role->system_name === 'admin') {
+            return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
+        }
+
         if ($entity->isA('book')) {
 
             if (!$entity->restricted) {
@@ -461,61 +471,77 @@ class PermissionService
         return $q;
     }
 
-    /**
-     * Add restrictions for a page query
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforcePageRestrictions($query, $action = 'view')
-    {
-        // Prevent drafts being visible to others.
-        $query = $query->where(function ($query) {
-            $query->where('draft', '=', false);
-            if ($this->currentUser()) {
-                $query->orWhere(function ($query) {
-                    $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
-                });
-            }
-        });
+    public function bookChildrenQuery($book_id, $filterDrafts = false) {
 
-        return $this->enforceEntityRestrictions($query, $action);
-    }
+        // Draft setup
+        $params = [
+            'userId' => $this->currentUser()->id,
+            'bookIdPage' => $book_id,
+            'bookIdChapter' => $book_id
+        ];
+        if (!$filterDrafts) {
+            $params['userIdDrafts'] = $this->currentUser()->id;
+        }
+        // Role setup
+        $userRoles = $this->getRoles();
+        $roleBindings = [];
+        $roleValues = [];
+        foreach ($userRoles as $index => $roleId) {
+            $roleBindings[':role'.$index] = $roleId;
+            $roleValues['role'.$index] = $roleId;
+        }
+        // TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
+        // Something which will handle the above role crap in a nice clean way
+        $roleBindingString = implode(',', array_keys($roleBindings));
+        $query = "SELECT * from (
+(SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
+    where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
+UNION
+(SELECT 'Bookstack\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft FROM {$this->chapter->getTable()} WHERE book_id = :bookIdChapter)
+) as U  WHERE (
+	SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
+    WHERE
+		jp.entity_id=U.id AND
+        jp.entity_type=U.entity_type AND
+		jp.action = 'view' AND
+        jp.role_id IN ({$roleBindingString}) AND
+        (
+			jp.has_permission = 1 OR
+            (jp.has_permission_own = 1 AND jp.created_by = :userId)
+        )
+) > 0
+ORDER BY draft desc, priority asc";
 
-    /**
-     * Add on permission restrictions to a chapter query.
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforceChapterRestrictions($query, $action = 'view')
-    {
-        return $this->enforceEntityRestrictions($query, $action);
-    }
-
-    /**
-     * Add restrictions to a book query.
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforceBookRestrictions($query, $action = 'view')
-    {
-        return $this->enforceEntityRestrictions($query, $action);
+        $this->clean();
+        return $this->db->select($query, array_replace($roleValues, $params));
     }
 
     /**
      * Add restrictions for a generic entity
-     * @param $query
+     * @param string $entityType
+     * @param Builder|Entity $query
      * @param string $action
      * @return mixed
      */
-    public function enforceEntityRestrictions($query, $action = 'view')
+    public function enforceEntityRestrictions($entityType, $query, $action = 'view')
     {
+        if (strtolower($entityType) === 'page') {
+            // Prevent drafts being visible to others.
+            $query = $query->where(function ($query) {
+                $query->where('draft', '=', false);
+                if ($this->currentUser()) {
+                    $query->orWhere(function ($query) {
+                        $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+                    });
+                }
+            });
+        }
+
         if ($this->isAdmin()) {
             $this->clean();
             return $query;
         }
+
         $this->currentAction = $action;
         return $this->entityRestrictionQuery($query);
     }
@@ -601,7 +627,7 @@ class PermissionService
     private function isAdmin()
     {
         if ($this->isAdminUser === null) {
-            $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false;
+            $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
         }
 
         return $this->isAdminUser;
diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php
index d76a7231b..5edd4cad7 100644
--- a/app/Services/SocialAuthService.php
+++ b/app/Services/SocialAuthService.php
@@ -70,12 +70,12 @@ class SocialAuthService
 
         // Check social account has not already been used
         if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
-            throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
+            throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
         }
 
         if ($this->userRepo->getByEmail($socialUser->getEmail())) {
             $email = $socialUser->getEmail();
-            throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
+            throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
         }
 
         return $socialUser;
@@ -98,7 +98,6 @@ class SocialAuthService
 
         // Get any attached social accounts or users
         $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
-        $user = $this->userRepo->getByEmail($socialUser->getEmail());
         $isLoggedIn = auth()->check();
         $currentUser = user();
 
@@ -113,27 +112,26 @@ class SocialAuthService
         if ($isLoggedIn && $socialAccount === null) {
             $this->fillSocialAccount($socialDriver, $socialUser);
             $currentUser->socialAccounts()->save($this->socialAccount);
-            session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
+            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in and the social account exists and is already linked to the current user.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
-            session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
+            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in, A social account exists but the users do not match.
-        // Change the user that the social account is assigned to.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
-            session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
+            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
             return redirect($currentUser->getEditUrl());
         }
 
         // Otherwise let the user know this social account is not used by anyone.
-        $message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
+        $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
         if (setting('registration-enabled')) {
-            $message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
+            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
         }
         
         throw new SocialSignInException($message . '.', '/login');
@@ -157,8 +155,8 @@ class SocialAuthService
     {
         $driver = trim(strtolower($socialDriver));
 
-        if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
-        if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
+        if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
+        if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
 
         return $driver;
     }
@@ -215,7 +213,7 @@ class SocialAuthService
     {
         session();
         user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
-        session()->flash('success', title_case($socialDriver) . ' account successfully detached');
+        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
         return redirect(user()->getEditUrl());
     }
 
diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php
index 1a9ee5f70..3285745ce 100644
--- a/app/Services/ViewService.php
+++ b/app/Services/ViewService.php
@@ -5,9 +5,7 @@ use BookStack\View;
 
 class ViewService
 {
-
     protected $view;
-    protected $user;
     protected $permissionService;
 
     /**
@@ -18,7 +16,6 @@ class ViewService
     public function __construct(View $view, PermissionService $permissionService)
     {
         $this->view = $view;
-        $this->user = user();
         $this->permissionService = $permissionService;
     }
 
@@ -29,8 +26,9 @@ class ViewService
      */
     public function add(Entity $entity)
     {
-        if ($this->user === null) return 0;
-        $view = $entity->views()->where('user_id', '=', $this->user->id)->first();
+        $user = user();
+        if ($user === null || $user->isDefault()) return 0;
+        $view = $entity->views()->where('user_id', '=', $user->id)->first();
         // Add view if model exists
         if ($view) {
             $view->increment('views');
@@ -39,7 +37,7 @@ class ViewService
 
         // Otherwise create new view count
         $entity->views()->save($this->view->create([
-            'user_id' => $this->user->id,
+            'user_id' => $user->id,
             'views' => 1
         ]));
 
@@ -78,13 +76,14 @@ class ViewService
      */
     public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
     {
-        if ($this->user === null) return collect();
+        $user = user();
+        if ($user === null || $user->isDefault()) return collect();
 
         $query = $this->permissionService
             ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
 
         if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
-        $query = $query->where('user_id', '=', user()->id);
+        $query = $query->where('user_id', '=', $user->id);
 
         $viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
             ->skip($count * $page)->take($count)->get()->pluck('viewable');
diff --git a/app/User.php b/app/User.php
index 09b189cbb..b5bb221e8 100644
--- a/app/User.php
+++ b/app/User.php
@@ -74,6 +74,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return $this->roles->pluck('name')->contains($role);
     }
 
+    /**
+     * Check if the user has a role.
+     * @param $role
+     * @return mixed
+     */
+    public function hasSystemRole($role)
+    {
+        return $this->roles->pluck('system_name')->contains('admin');
+    }
+
     /**
      * Get all permissions belonging to a the current user.
      * @param bool $cache
diff --git a/composer.json b/composer.json
index 7d4b5e62b..5a8fd67ae 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,8 @@
         "league/flysystem-aws-s3-v3": "^1.0",
         "barryvdh/laravel-dompdf": "^0.7",
         "predis/predis": "^1.1",
-        "gathercontent/htmldiff": "^0.2.1"
+        "gathercontent/htmldiff": "^0.2.1",
+        "barryvdh/laravel-snappy": "^0.3.1"
     },
     "require-dev": {
         "fzaninotto/faker": "~1.4",
diff --git a/composer.lock b/composer.lock
index 74a090288..dcde9d9c6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "3124d900cfe857392a94de479f3ff6d4",
-    "content-hash": "a968767a73f77e66e865c276cf76eedf",
+    "hash": "2438a2f4a02adbea5f378f9e9408eb29",
+    "content-hash": "6add8bff71ecc86e0c90858590834a26",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
@@ -255,6 +255,58 @@
             ],
             "time": "2016-07-04 11:52:48"
         },
+        {
+            "name": "barryvdh/laravel-snappy",
+            "version": "v0.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/barryvdh/laravel-snappy.git",
+                "reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/509a4497be63d8ee7ff464a3daf00d9edde08e21",
+                "reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x",
+                "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x",
+                "knplabs/knp-snappy": "*",
+                "php": ">=5.4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Barryvdh\\Snappy\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Barry vd. Heuvel",
+                    "email": "barryvdh@gmail.com"
+                }
+            ],
+            "description": "Snappy PDF/Image for Laravel 4",
+            "keywords": [
+                "image",
+                "laravel",
+                "pdf",
+                "snappy",
+                "wkhtmltoimage",
+                "wkhtmltopdf"
+            ],
+            "time": "2016-08-05 13:08:28"
+        },
         {
             "name": "barryvdh/reflection-docblock",
             "version": "v2.0.4",
@@ -997,6 +1049,71 @@
             ],
             "time": "2015-12-05 17:17:57"
         },
+        {
+            "name": "knplabs/knp-snappy",
+            "version": "0.4.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/KnpLabs/snappy.git",
+                "reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/44f7a9b37d5686fd7db4c1e9569a802a5d16923f",
+                "reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "symfony/process": "~2.3|~3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.7"
+            },
+            "suggest": {
+                "h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltoimage-i386": "Provides wkhtmltoimage-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltopdf-amd64": "Provides wkhtmltopdf-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltopdf-i386": "Provides wkhtmltopdf-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "wemersonjanuario/wkhtmltopdf-windows": "Provides wkhtmltopdf executable for Windows, use version `~0.12` as dependency"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Knp\\Snappy": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "KNPLabs Team",
+                    "homepage": "http://knplabs.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "http://github.com/KnpLabs/snappy/contributors"
+                }
+            ],
+            "description": "PHP5 library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.",
+            "homepage": "http://github.com/KnpLabs/snappy",
+            "keywords": [
+                "knp",
+                "knplabs",
+                "pdf",
+                "snapshot",
+                "thumbnail",
+                "wkhtmltopdf"
+            ],
+            "time": "2015-11-17 13:16:27"
+        },
         {
             "name": "laravel/framework",
             "version": "v5.3.11",
diff --git a/config/app.php b/config/app.php
index d94196cc1..2a7a36ea2 100644
--- a/config/app.php
+++ b/config/app.php
@@ -148,6 +148,7 @@ return [
         Barryvdh\DomPDF\ServiceProvider::class,
         Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
         Barryvdh\Debugbar\ServiceProvider::class,
+        Barryvdh\Snappy\ServiceProvider::class,
 
 
         /*
@@ -218,6 +219,7 @@ return [
 
         'ImageTool' => Intervention\Image\Facades\Image::class,
         'PDF' => Barryvdh\DomPDF\Facade::class,
+        'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
         'Debugbar'  => Barryvdh\Debugbar\Facade::class,
 
         /**
diff --git a/config/setting-defaults.php b/config/setting-defaults.php
index c681bb7f5..db35023d5 100644
--- a/config/setting-defaults.php
+++ b/config/setting-defaults.php
@@ -6,6 +6,7 @@
 return [
 
     'app-name'        => 'BookStack',
+    'app-logo' => '',
     'app-name-header' => true,
     'app-editor'      => 'wysiwyg',
     'app-color'       => '#0288D1',
diff --git a/config/snappy.php b/config/snappy.php
new file mode 100644
index 000000000..73f21fd30
--- /dev/null
+++ b/config/snappy.php
@@ -0,0 +1,18 @@
+<?php
+
+return [
+    'pdf' => [
+        'enabled' => true,
+        'binary'  => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
+        'timeout' => false,
+        'options' => [],
+        'env'     => [],
+    ],
+    'image' => [
+        'enabled' => false,
+        'binary'  => '/usr/local/bin/wkhtmltoimage',
+        'timeout' => false,
+        'options' => [],
+        'env'     => [],
+    ],
+];
diff --git a/package.json b/package.json
index 30f288d45..ec5911b93 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,9 @@
 {
   "private": true,
   "scripts": {
-    "prod": "gulp --production",
-    "dev": "gulp watch"
+    "build": "gulp --production",
+    "dev": "gulp watch",
+    "watch": "gulp watch"
   },
   "devDependencies": {
     "angular": "^1.5.5",
diff --git a/phpunit.xml b/phpunit.xml
index 72e06a3fc..2e07cdbf8 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -22,6 +22,7 @@
     <php>
         <env name="APP_ENV" value="testing"/>
         <env name="APP_DEBUG" value="false"/>
+        <env name="APP_LANG" value="en"/>
         <env name="CACHE_DRIVER" value="array"/>
         <env name="SESSION_DRIVER" value="array"/>
         <env name="QUEUE_DRIVER" value="sync"/>
diff --git a/readme.md b/readme.md
index 5d3e79a2e..fa5c48fe0 100644
--- a/readme.md
+++ b/readme.md
@@ -17,22 +17,33 @@ A platform for storing and organising information and documentation. General inf
 
 All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
 
-* [Node.js](https://nodejs.org/en/)
-* [Gulp](http://gulpjs.com/)
+* [Node.js](https://nodejs.org/en/) v6.9+
 
-SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
+SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. To run the build task you can use the following commands:
+
+``` bash
+# Build and minify for production
+npm run-script build
+
+# Build for dev (With sourcemaps) and watch for changes
+npm run-script dev
+```
 
 BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
 
 The testing database will also need migrating and seeding beforehand. This can be done with the following commands:
 
-```
+``` bash
 php artisan migrate --database=mysql_testing
 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
 ```
 
 Once done you can run `phpunit` in the application root directory to run all tests.
 
+## Website and Docs 
+
+The website and project docs are currently stored in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. The docs are stored as markdown files in the `resources/docs` folder
+
 ## License
 
 BookStack is provided under the MIT License.
@@ -53,5 +64,11 @@ These are the great projects used to help build BookStack:
 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
 * [Marked](https://github.com/chjj/marked)
 * [Moment.js](http://momentjs.com/)
+* [BarryVD](https://github.com/barryvdh)
+    * [Debugbar](https://github.com/barryvdh/laravel-debugbar)
+    * [Dompdf](https://github.com/barryvdh/laravel-dompdf)
+    * [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
+    * [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
+* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
 
 Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy.
diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 9d7f7ad70..0d57b09ad 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -2,6 +2,8 @@
 
 import moment from 'moment';
 import 'moment/locale/en-gb';
+import editorOptions from "./pages/page-form";
+
 moment.locale('en-gb');
 
 export default function (ngApp, events) {
@@ -23,14 +25,14 @@ export default function (ngApp, events) {
             $scope.searching = false;
             $scope.searchTerm = '';
 
-            var page = 0;
-            var previousClickTime = 0;
-            var previousClickImage = 0;
-            var dataLoaded = false;
-            var callback = false;
+            let page = 0;
+            let previousClickTime = 0;
+            let previousClickImage = 0;
+            let dataLoaded = false;
+            let callback = false;
 
-            var preSearchImages = [];
-            var preSearchHasMore = false;
+            let preSearchImages = [];
+            let preSearchHasMore = false;
 
             /**
              * Used by dropzone to get the endpoint to upload to.
@@ -62,7 +64,7 @@ export default function (ngApp, events) {
                 $scope.$apply(() => {
                     $scope.images.unshift(data);
                 });
-                events.emit('success', 'Image uploaded');
+                events.emit('success', trans('components.image_upload_success'));
             };
 
             /**
@@ -79,9 +81,9 @@ export default function (ngApp, events) {
              * @param image
              */
             $scope.imageSelect = function (image) {
-                var dblClickTime = 300;
-                var currentTime = Date.now();
-                var timeDiff = currentTime - previousClickTime;
+                let dblClickTime = 300;
+                let currentTime = Date.now();
+                let timeDiff = currentTime - previousClickTime;
 
                 if (timeDiff < dblClickTime && image.id === previousClickImage) {
                     // If double click
@@ -137,22 +139,21 @@ export default function (ngApp, events) {
                 $('#image-manager').find('.overlay').fadeOut(240);
             };
 
-            var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
+            let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
 
             /**
              * Fetch the list image data from the server.
              */
             function fetchData() {
-                var url = baseUrl + page + '?';
-                var components = {};
+                let url = baseUrl + page + '?';
+                let components = {};
                 if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
                 if ($scope.searching) components['term'] = $scope.searchTerm;
 
 
-                var urlQueryString = Object.keys(components).map((key) => {
+                url += Object.keys(components).map((key) => {
                     return key + '=' + encodeURIComponent(components[key]);
                 }).join('&');
-                url += urlQueryString;
 
                 $http.get(url).then((response) => {
                     $scope.images = $scope.images.concat(response.data.images);
@@ -205,13 +206,13 @@ export default function (ngApp, events) {
              */
             $scope.saveImageDetails = function (event) {
                 event.preventDefault();
-                var url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
+                let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
                 $http.put(url, this.selectedImage).then(response => {
-                    events.emit('success', 'Image details updated');
+                    events.emit('success', trans('components.image_update_success'));
                 }, (response) => {
                     if (response.status === 422) {
-                        var errors = response.data;
-                        var message = '';
+                        let errors = response.data;
+                        let message = '';
                         Object.keys(errors).forEach((key) => {
                             message += errors[key].join('\n');
                         });
@@ -230,13 +231,13 @@ export default function (ngApp, events) {
              */
             $scope.deleteImage = function (event) {
                 event.preventDefault();
-                var force = $scope.dependantPages !== false;
-                var url = window.baseUrl('/images/' + $scope.selectedImage.id);
+                let force = $scope.dependantPages !== false;
+                let url = window.baseUrl('/images/' + $scope.selectedImage.id);
                 if (force) url += '?force=true';
                 $http.delete(url).then((response) => {
                     $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
                     $scope.selectedImage = false;
-                    events.emit('success', 'Image successfully deleted');
+                    events.emit('success', trans('components.image_delete_success'));
                 }, (response) => {
                     // Pages failure
                     if (response.status === 400) {
@@ -266,11 +267,11 @@ export default function (ngApp, events) {
 
         $scope.searchBook = function (e) {
             e.preventDefault();
-            var term = $scope.searchTerm;
+            let term = $scope.searchTerm;
             if (term.length == 0) return;
             $scope.searching = true;
             $scope.searchResults = '';
-            var searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
+            let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
             searchUrl += '?term=' + encodeURIComponent(term);
             $http.get(searchUrl).then((response) => {
                 $scope.searchResults = $sce.trustAsHtml(response.data);
@@ -294,27 +295,27 @@ export default function (ngApp, events) {
     ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
         function ($scope, $http, $attrs, $interval, $timeout, $sce) {
 
-        $scope.editorOptions = require('./pages/page-form');
+        $scope.editorOptions = editorOptions();
         $scope.editContent = '';
         $scope.draftText = '';
-        var pageId = Number($attrs.pageId);
-        var isEdit = pageId !== 0;
-        var autosaveFrequency = 30; // AutoSave interval in seconds.
-        var isMarkdown = $attrs.editorType === 'markdown';
+        let pageId = Number($attrs.pageId);
+        let isEdit = pageId !== 0;
+        let autosaveFrequency = 30; // AutoSave interval in seconds.
+        let isMarkdown = $attrs.editorType === 'markdown';
         $scope.draftsEnabled = $attrs.draftsEnabled === 'true';
         $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
         $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
 
         // Set initial header draft text
         if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
-            $scope.draftText = 'Editing Draft'
+            $scope.draftText = trans('entities.pages_editing_draft');
         } else {
-            $scope.draftText = 'Editing Page'
+            $scope.draftText = trans('entities.pages_editing_page');
         }
 
-        var autoSave = false;
+        let autoSave = false;
 
-        var currentContent = {
+        let currentContent = {
             title: false,
             html: false
         };
@@ -351,8 +352,8 @@ export default function (ngApp, events) {
             autoSave = $interval(() => {
                 // Return if manually saved recently to prevent bombarding the server
                 if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
-                var newTitle = $('#name').val();
-                var newHtml = $scope.editContent;
+                let newTitle = $('#name').val();
+                let newHtml = $scope.editContent;
 
                 if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
                     currentContent.html = newHtml;
@@ -369,7 +370,7 @@ export default function (ngApp, events) {
          */
         function saveDraft() {
             if (!$scope.draftsEnabled) return;
-            var data = {
+            let data = {
                 name: $('#name').val(),
                 html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
             };
@@ -379,14 +380,14 @@ export default function (ngApp, events) {
             let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
             $http.put(url, data).then(responseData => {
                 draftErroring = false;
-                var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
+                let updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
                 $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
                 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
                 showDraftSaveNotification();
                 lastSave = Date.now();
             }, errorRes => {
                 if (draftErroring) return;
-                events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.')
+                events.emit('error', trans('errors.page_draft_autosave_fail'));
                 draftErroring = true;
             });
         }
@@ -419,7 +420,7 @@ export default function (ngApp, events) {
             let url = window.baseUrl('/ajax/page/' + pageId);
             $http.get(url).then((responseData) => {
                 if (autoSave) $interval.cancel(autoSave);
-                $scope.draftText = 'Editing Page';
+                $scope.draftText = trans('entities.pages_editing_page');
                 $scope.isUpdateDraft = false;
                 $scope.$broadcast('html-update', responseData.data.html);
                 $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
@@ -427,7 +428,7 @@ export default function (ngApp, events) {
                 $timeout(() => {
                     startAutoSave();
                 }, 1000);
-                events.emit('success', 'Draft discarded, The editor has been updated with the current page content');
+                events.emit('success', trans('entities.pages_draft_discarded'));
             });
         };
 
@@ -505,20 +506,6 @@ export default function (ngApp, events) {
                 }
             };
 
-            /**
-             * Save the tags to the current page.
-             */
-            $scope.saveTags = function() {
-                setTagOrder();
-                let postData = {tags: $scope.tags};
-                let url = window.baseUrl('/ajax/tags/update/page/' + pageId);
-                $http.post(url, postData).then((responseData) => {
-                    $scope.tags = responseData.data.tags;
-                    addEmptyTag();
-                    events.emit('success', responseData.data.message);
-                })
-            };
-
             /**
              * Remove a tag from the current list.
              * @param tag
@@ -588,7 +575,7 @@ export default function (ngApp, events) {
              * Get files for the current page from the server.
              */
             function getFiles() {
-                let url = window.baseUrl(`/attachments/get/page/${pageId}`)
+                let url = window.baseUrl(`/attachments/get/page/${pageId}`);
                 $http.get(url).then(resp => {
                     $scope.files = resp.data;
                     currentOrder = resp.data.map(file => {return file.id}).join(':');
@@ -606,7 +593,7 @@ export default function (ngApp, events) {
                 $scope.$apply(() => {
                     $scope.files.push(data);
                 });
-                events.emit('success', 'File uploaded');
+                events.emit('success', trans('entities.attachments_file_uploaded'));
             };
 
             /**
@@ -624,7 +611,7 @@ export default function (ngApp, events) {
                         data.link = '';
                     }
                 });
-                events.emit('success', 'File updated');
+                events.emit('success', trans('entities.attachments_file_updated'));
             };
 
             /**
@@ -650,7 +637,7 @@ export default function (ngApp, events) {
                 file.uploaded_to = pageId;
                 $http.post(window.baseUrl('/attachments/link'), file).then(resp => {
                     $scope.files.push(resp.data);
-                    events.emit('success', 'Link attached');
+                    events.emit('success', trans('entities.attachments_link_attached'));
                     $scope.file = getCleanFile();
                 }, checkError('link'));
             };
@@ -684,7 +671,7 @@ export default function (ngApp, events) {
                         $scope.editFile.link = '';
                     }
                     $scope.editFile = false;
-                    events.emit('success', 'Attachment details updated');
+                    events.emit('success', trans('entities.attachments_updated_success'));
                 }, checkError('edit'));
             };
 
diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js
index 44d1a14e1..ef8bcd85c 100644
--- a/resources/assets/js/directives.js
+++ b/resources/assets/js/directives.js
@@ -1,38 +1,8 @@
 "use strict";
-const DropZone = require('dropzone');
-const markdown = require('marked');
+import DropZone from "dropzone";
+import markdown from "marked";
 
-module.exports = function (ngApp, events) {
-
-    /**
-     * Toggle Switches
-     * Has basic on/off functionality.
-     * Use string values of 'true' & 'false' to dictate the current state.
-     */
-    ngApp.directive('toggleSwitch', function () {
-        return {
-            restrict: 'A',
-            template: `
-            <div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
-                <input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
-                <div class="switch-handle"></div>
-            </div>
-            `,
-            scope: true,
-            link: function (scope, element, attrs) {
-                scope.name = attrs.name;
-                scope.value = attrs.value;
-                scope.isActive = scope.value == true && scope.value != 'false';
-                scope.value = (scope.value == true && scope.value != 'false') ? 'true' : 'false';
-
-                scope.switch = function () {
-                    scope.isActive = !scope.isActive;
-                    scope.value = scope.isActive ? 'true' : 'false';
-                }
-
-            }
-        };
-    });
+export default function (ngApp, events) {
 
     /**
      * Common tab controls using simple jQuery functions.
@@ -65,7 +35,7 @@ module.exports = function (ngApp, events) {
     });
 
     /**
-     * Sub form component to allow inner-form sections to act like thier own forms.
+     * Sub form component to allow inner-form sections to act like their own forms.
      */
     ngApp.directive('subForm', function() {
         return {
@@ -80,96 +50,13 @@ module.exports = function (ngApp, events) {
                 element.find('button[type="submit"]').click(submitEvent);
 
                 function submitEvent(e) {
-                    e.preventDefault()
+                    e.preventDefault();
                     if (attrs.subForm) scope.$eval(attrs.subForm);
                 }
             }
         };
     });
 
-
-    /**
-     * Image Picker
-     * Is a simple front-end interface that connects to an ImageManager if present.
-     */
-    ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
-        return {
-            restrict: 'E',
-            template: `
-            <div class="image-picker">
-                <div>
-                    <img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
-                    <img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
-                </div>
-                <button class="button" type="button" ng-click="showImageManager()">Select Image</button>
-                <br>
-
-                <button class="text-button" ng-click="reset()" type="button">Reset</button>
-                <span ng-show="showRemove" class="sep">|</span>
-                <button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
-
-                <input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
-            </div>
-            `,
-            scope: {
-                name: '@',
-                resizeHeight: '@',
-                resizeWidth: '@',
-                resizeCrop: '@',
-                showRemove: '=',
-                currentImage: '@',
-                currentId: '@',
-                defaultImage: '@',
-                imageClass: '@'
-            },
-            link: function (scope, element, attrs) {
-                let usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
-                scope.image = scope.currentImage;
-                scope.value = scope.currentImage || '';
-                if (usingIds) scope.value = scope.currentId;
-
-                function setImage(imageModel, imageUrl) {
-                    scope.image = imageUrl;
-                    scope.value = usingIds ? imageModel.id : imageUrl;
-                }
-
-                scope.reset = function () {
-                    setImage({id: 0}, scope.defaultImage);
-                };
-
-                scope.remove = function () {
-                    scope.image = 'none';
-                    scope.value = 'none';
-                };
-
-                scope.showImageManager = function () {
-                    imageManagerService.show((image) => {
-                        scope.updateImageFromModel(image);
-                    });
-                };
-
-                scope.updateImageFromModel = function (model) {
-                    let isResized = scope.resizeWidth && scope.resizeHeight;
-
-                    if (!isResized) {
-                        scope.$apply(() => {
-                            setImage(model, model.url);
-                        });
-                        return;
-                    }
-
-                    let cropped = scope.resizeCrop ? 'true' : 'false';
-                    let requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped;
-                    requestString = window.baseUrl(requestString);
-                    $http.get(requestString).then((response) => {
-                        setImage(model, response.data.url);
-                    });
-                };
-
-            }
-        };
-    }]);
-
     /**
      * DropZone
      * Used for uploading images
@@ -179,25 +66,26 @@ module.exports = function (ngApp, events) {
             restrict: 'E',
             template: `
             <div class="dropzone-container">
-                <div class="dz-message">Drop files or click here to upload</div>
+                <div class="dz-message">{{message}}</div>
             </div>
             `,
             scope: {
                 uploadUrl: '@',
                 eventSuccess: '=',
                 eventError: '=',
-                uploadedTo: '@'
+                uploadedTo: '@',
             },
             link: function (scope, element, attrs) {
+                scope.message = attrs.message;
                 if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
-                var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
+                let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
                     url: scope.uploadUrl,
                     init: function () {
-                        var dz = this;
+                        let dz = this;
                         dz.on('sending', function (file, xhr, data) {
-                            var token = window.document.querySelector('meta[name=token]').getAttribute('content');
+                            let token = window.document.querySelector('meta[name=token]').getAttribute('content');
                             data.append('_token', token);
-                            var uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
+                            let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
                             data.append('uploaded_to', uploadedTo);
                         });
                         if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
@@ -214,7 +102,7 @@ module.exports = function (ngApp, events) {
                                 $(file.previewElement).find('[data-dz-errormessage]').text(message);
                             }
 
-                            if (xhr.status === 413) setMessage('The server does not allow uploads of this size. Please try a smaller file.');
+                            if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
                             if (errorMessage.file) setMessage(errorMessage.file[0]);
 
                         });
@@ -273,7 +161,7 @@ module.exports = function (ngApp, events) {
 
                 function tinyMceSetup(editor) {
                     editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
-                        var content = editor.getContent();
+                        let content = editor.getContent();
                         $timeout(() => {
                             scope.mceModel = content;
                         });
@@ -301,9 +189,9 @@ module.exports = function (ngApp, events) {
                 // Custom tinyMCE plugins
                 tinymce.PluginManager.add('customhr', function (editor) {
                     editor.addCommand('InsertHorizontalRule', function () {
-                        var hrElem = document.createElement('hr');
-                        var cNode = editor.selection.getNode();
-                        var parentNode = cNode.parentNode;
+                        let hrElem = document.createElement('hr');
+                        let cNode = editor.selection.getNode();
+                        let parentNode = cNode.parentNode;
                         parentNode.insertBefore(hrElem, cNode);
                     });
 
@@ -373,15 +261,21 @@ module.exports = function (ngApp, events) {
             link: function (scope, element, attrs) {
 
                 // Elements
-                const input = element.find('[markdown-input] textarea').first();
-                const display = element.find('.markdown-display').first();
-                const insertImage = element.find('button[data-action="insertImage"]');
-                const insertEntityLink = element.find('button[data-action="insertEntityLink"]')
+                const $input = element.find('[markdown-input] textarea').first();
+                const $display = element.find('.markdown-display').first();
+                const $insertImage = element.find('button[data-action="insertImage"]');
+                const $insertEntityLink = element.find('button[data-action="insertEntityLink"]');
+
+                // Prevent markdown display link click redirect
+                $display.on('click', 'a', function(event) {
+                    event.preventDefault();
+                    window.open(this.getAttribute('href'));
+                });
 
                 let currentCaretPos = 0;
 
-                input.blur(event => {
-                    currentCaretPos = input[0].selectionStart;
+                $input.blur(event => {
+                    currentCaretPos = $input[0].selectionStart;
                 });
 
                 // Scroll sync
@@ -391,10 +285,10 @@ module.exports = function (ngApp, events) {
                     displayHeight;
 
                 function setScrollHeights() {
-                    inputScrollHeight = input[0].scrollHeight;
-                    inputHeight = input.height();
-                    displayScrollHeight = display[0].scrollHeight;
-                    displayHeight = display.height();
+                    inputScrollHeight = $input[0].scrollHeight;
+                    inputHeight = $input.height();
+                    displayScrollHeight = $display[0].scrollHeight;
+                    displayHeight = $display.height();
                 }
 
                 setTimeout(() => {
@@ -403,29 +297,29 @@ module.exports = function (ngApp, events) {
                 window.addEventListener('resize', setScrollHeights);
                 let scrollDebounceTime = 800;
                 let lastScroll = 0;
-                input.on('scroll', event => {
+                $input.on('scroll', event => {
                     let now = Date.now();
                     if (now - lastScroll > scrollDebounceTime) {
                         setScrollHeights()
                     }
-                    let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
+                    let scrollPercent = ($input.scrollTop() / (inputScrollHeight - inputHeight));
                     let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
-                    display.scrollTop(displayScrollY);
+                    $display.scrollTop(displayScrollY);
                     lastScroll = now;
                 });
 
                 // Editor key-presses
-                input.keydown(event => {
+                $input.keydown(event => {
                     // Insert image shortcut
                     if (event.which === 73 && event.ctrlKey && event.shiftKey) {
                         event.preventDefault();
-                        let caretPos = input[0].selectionStart;
-                        let currentContent = input.val();
+                        let caretPos = $input[0].selectionStart;
+                        let currentContent = $input.val();
                         const mdImageText = "![](http://)";
-                        input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
-                        input.focus();
-                        input[0].selectionStart = caretPos + ("![](".length);
-                        input[0].selectionEnd = caretPos + ('![](http://'.length);
+                        $input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
+                        $input.focus();
+                        $input[0].selectionStart = caretPos + ("![](".length);
+                        $input[0].selectionEnd = caretPos + ('![](http://'.length);
                         return;
                     }
 
@@ -440,48 +334,48 @@ module.exports = function (ngApp, events) {
                 });
 
                 // Insert image from image manager
-                insertImage.click(event => {
+                $insertImage.click(event => {
                     window.ImageManager.showExternal(image => {
                         let caretPos = currentCaretPos;
-                        let currentContent = input.val();
+                        let currentContent = $input.val();
                         let mdImageText = "![" + image.name + "](" + image.thumbs.display + ")";
-                        input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
-                        input.change();
+                        $input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
+                        $input.change();
                     });
                 });
 
                 function showLinkSelector() {
                     window.showEntityLinkSelector((entity) => {
                         let selectionStart = currentCaretPos;
-                        let selectionEnd = input[0].selectionEnd;
+                        let selectionEnd = $input[0].selectionEnd;
                         let textSelected = (selectionEnd !== selectionStart);
-                        let currentContent = input.val();
+                        let currentContent = $input.val();
 
                         if (textSelected) {
                             let selectedText = currentContent.substring(selectionStart, selectionEnd);
                             let linkText = `[${selectedText}](${entity.link})`;
-                            input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd));
+                            $input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd));
                         } else {
                             let linkText = ` [${entity.name}](${entity.link}) `;
-                            input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
+                            $input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
                         }
-                        input.change();
+                        $input.change();
                     });
                 }
-                insertEntityLink.click(showLinkSelector);
+                $insertEntityLink.click(showLinkSelector);
 
                 // Upload and insert image on paste
                 function editorPaste(e) {
                     e = e.originalEvent;
                     if (!e.clipboardData) return
-                    var items = e.clipboardData.items;
+                    let items = e.clipboardData.items;
                     if (!items) return;
-                    for (var i = 0; i < items.length; i++) {
+                    for (let i = 0; i < items.length; i++) {
                         uploadImage(items[i].getAsFile());
                     }
                 }
 
-                input.on('paste', editorPaste);
+                $input.on('paste', editorPaste);
 
                 // Handle image drop, Uploads images to BookStack.
                 function handleImageDrop(event) {
@@ -493,17 +387,17 @@ module.exports = function (ngApp, events) {
                     }
                 }
 
-                input.on('drop', handleImageDrop);
+                $input.on('drop', handleImageDrop);
 
                 // Handle image upload and add image into markdown content
                 function uploadImage(file) {
                     if (file.type.indexOf('image') !== 0) return;
-                    var formData = new FormData();
-                    var ext = 'png';
-                    var xhr = new XMLHttpRequest();
+                    let formData = new FormData();
+                    let ext = 'png';
+                    let xhr = new XMLHttpRequest();
 
                     if (file.name) {
-                        var fileNameMatches = file.name.match(/\.(.+)$/);
+                        let fileNameMatches = file.name.match(/\.(.+)$/);
                         if (fileNameMatches) {
                             ext = fileNameMatches[1];
                         }
@@ -511,17 +405,17 @@ module.exports = function (ngApp, events) {
 
                     // Insert image into markdown
                     let id = "image-" + Math.random().toString(16).slice(2);
-                    let selectStart = input[0].selectionStart;
-                    let selectEnd = input[0].selectionEnd;
-                    let content = input[0].value;
+                    let selectStart = $input[0].selectionStart;
+                    let selectEnd = $input[0].selectionEnd;
+                    let content = $input[0].value;
                     let selectText = content.substring(selectStart, selectEnd);
                     let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
                     let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`;
-                    input[0].value = content.substring(0, selectStart) +  innerContent + content.substring(selectEnd);
+                    $input[0].value = content.substring(0, selectStart) +  innerContent + content.substring(selectEnd);
 
-                    input.focus();
-                    input[0].selectionStart = selectStart;
-                    input[0].selectionEnd = selectStart;
+                    $input.focus();
+                    $input[0].selectionStart = selectStart;
+                    $input[0].selectionEnd = selectStart;
 
                     let remoteFilename = "image-" + Date.now() + "." + ext;
                     formData.append('file', file, remoteFilename);
@@ -529,20 +423,20 @@ module.exports = function (ngApp, events) {
 
                     xhr.open('POST', window.baseUrl('/images/gallery/upload'));
                     xhr.onload = function () {
-                        let selectStart = input[0].selectionStart;
+                        let selectStart = $input[0].selectionStart;
                         if (xhr.status === 200 || xhr.status === 201) {
-                            var result = JSON.parse(xhr.responseText);
-                            input[0].value = input[0].value.replace(placeholderImage, result.thumbs.display);
-                            input.change();
+                            let result = JSON.parse(xhr.responseText);
+                            $input[0].value = $input[0].value.replace(placeholderImage, result.thumbs.display);
+                            $input.change();
                         } else {
-                            console.log('An error occurred uploading the image');
+                            console.log(trans('errors.image_upload_error'));
                             console.log(xhr.responseText);
-                            input[0].value = input[0].value.replace(innerContent, '');
-                            input.change();
+                            $input[0].value = $input[0].value.replace(innerContent, '');
+                            $input.change();
                         }
-                        input.focus();
-                        input[0].selectionStart = selectStart;
-                        input[0].selectionEnd = selectStart;
+                        $input.focus();
+                        $input[0].selectionStart = selectStart;
+                        $input[0].selectionEnd = selectStart;
                     };
                     xhr.send(formData);
                 }
@@ -680,8 +574,7 @@ module.exports = function (ngApp, events) {
                     }
                     // Enter or tab key
                     else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
-                        let text = suggestionElems[active].textContent;
-                        currentInput[0].value = text;
+                        currentInput[0].value = suggestionElems[active].textContent;
                         currentInput.focus();
                         $suggestionBox.hide();
                         isShowing = false;
@@ -732,14 +625,13 @@ module.exports = function (ngApp, events) {
                     // Build suggestions
                     $suggestionBox[0].innerHTML = '';
                     for (let i = 0; i < suggestions.length; i++) {
-                        var suggestion = document.createElement('li');
+                        let suggestion = document.createElement('li');
                         suggestion.textContent = suggestions[i];
                         suggestion.onclick = suggestionClick;
                         if (i === 0) {
-                            suggestion.className = 'active'
+                            suggestion.className = 'active';
                             active = 0;
                         }
-                        ;
                         $suggestionBox[0].appendChild(suggestion);
                     }
 
@@ -748,12 +640,11 @@ module.exports = function (ngApp, events) {
 
                 // Suggestion click event
                 function suggestionClick(event) {
-                    let text = this.textContent;
-                    currentInput[0].value = text;
+                    currentInput[0].value = this.textContent;
                     currentInput.focus();
                     $suggestionBox.hide();
                     isShowing = false;
-                };
+                }
 
                 // Get suggestions & cache
                 function getSuggestions(input, url) {
@@ -779,7 +670,7 @@ module.exports = function (ngApp, events) {
 
     ngApp.directive('entityLinkSelector', [function($http) {
         return {
-            restict: 'A',
+            restrict: 'A',
             link: function(scope, element, attrs) {
 
                 const selectButton = element.find('.entity-link-selector-confirm');
@@ -843,7 +734,7 @@ module.exports = function (ngApp, events) {
                 const input = element.find('[entity-selector-input]').first();
 
                 // Detect double click events
-                var lastClick = 0;
+                let lastClick = 0;
                 function isDoubleClick() {
                     let now = Date.now();
                     let answer = now - lastClick < 300;
diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js
index 9aa5dff52..94462ed64 100644
--- a/resources/assets/js/global.js
+++ b/resources/assets/js/global.js
@@ -1,11 +1,11 @@
 "use strict";
 
 // AngularJS - Create application and load components
-var angular = require('angular');
-var ngResource = require('angular-resource');
-var ngAnimate = require('angular-animate');
-var ngSanitize = require('angular-sanitize');
-require('angular-ui-sortable');
+import angular from "angular";
+import "angular-resource";
+import "angular-animate";
+import "angular-sanitize";
+import "angular-ui-sortable";
 
 // Url retrieval function
 window.baseUrl = function(path) {
@@ -15,7 +15,13 @@ window.baseUrl = function(path) {
     return basePath + '/' + path;
 };
 
-var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
+let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
+
+// Translation setup
+// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
+import Translations from "./translations"
+let translator = new Translations(window.translations);
+window.trans = translator.get.bind(translator);
 
 // Global Event System
 class EventManager {
@@ -25,9 +31,9 @@ class EventManager {
 
     emit(eventName, eventData) {
         if (typeof this.listeners[eventName] === 'undefined') return this;
-        var eventsToStart = this.listeners[eventName];
+        let eventsToStart = this.listeners[eventName];
         for (let i = 0; i < eventsToStart.length; i++) {
-            var event = eventsToStart[i];
+            let event = eventsToStart[i];
             event(eventData);
         }
         return this;
@@ -70,93 +76,83 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
 });
 
 // Global jQuery Elements
-$(function () {
-
-    var notifications = $('.notification');
-    var successNotification = notifications.filter('.pos');
-    var errorNotification = notifications.filter('.neg');
-    var warningNotification = notifications.filter('.warning');
-    // Notification Events
-    window.Events.listen('success', function (text) {
-        successNotification.hide();
-        successNotification.find('span').text(text);
-        setTimeout(() => {
-            successNotification.show();
-        }, 1);
-    });
-    window.Events.listen('warning', function (text) {
-        warningNotification.find('span').text(text);
-        warningNotification.show();
-    });
-    window.Events.listen('error', function (text) {
-        errorNotification.find('span').text(text);
-        errorNotification.show();
-    });
-
-    // Notification hiding
-    notifications.click(function () {
-        $(this).fadeOut(100);
-    });
-
-    // Chapter page list toggles
-    $('.chapter-toggle').click(function (e) {
-        e.preventDefault();
-        $(this).toggleClass('open');
-        $(this).closest('.chapter').find('.inset-list').slideToggle(180);
-    });
-
-    // Back to top button
-    $('#back-to-top').click(function() {
-         $('#header').smoothScrollTo();
-    });
-    var scrollTopShowing = false;
-    var scrollTop = document.getElementById('back-to-top');
-    var scrollTopBreakpoint = 1200;
-    window.addEventListener('scroll', function() {
-        let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
-        if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
-            scrollTop.style.display = 'block';
-            scrollTopShowing = true;
-            setTimeout(() => {
-                scrollTop.style.opacity = 0.4;
-            }, 1);
-        } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
-            scrollTop.style.opacity = 0;
-            scrollTopShowing = false;
-            setTimeout(() => {
-                scrollTop.style.display = 'none';
-            }, 500);
-        }
-    });
-
-    // Common jQuery actions
-    $('[data-action="expand-entity-list-details"]').click(function() {
-        $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
-    });
-
-    // Popup close
-    $('.popup-close').click(function() {
-        $(this).closest('.overlay').fadeOut(240);
-    });
-    $('.overlay').click(function(event) {
-        if (!$(event.target).hasClass('overlay')) return;
-        $(this).fadeOut(240);
-    });
-
-    // Prevent markdown display link click redirect
-    $('.markdown-display').on('click', 'a', function(event) {
-        event.preventDefault();
-        window.open($(this).attr('href'));
-    });
-
-    // Detect IE for css
-    if(navigator.userAgent.indexOf('MSIE')!==-1
-        || navigator.appVersion.indexOf('Trident/') > 0
-        || navigator.userAgent.indexOf('Safari') !== -1){
-        $('body').addClass('flexbox-support');
-    }
-
+let notifications = $('.notification');
+let successNotification = notifications.filter('.pos');
+let errorNotification = notifications.filter('.neg');
+let warningNotification = notifications.filter('.warning');
+// Notification Events
+window.Events.listen('success', function (text) {
+    successNotification.hide();
+    successNotification.find('span').text(text);
+    setTimeout(() => {
+        successNotification.show();
+    }, 1);
+});
+window.Events.listen('warning', function (text) {
+    warningNotification.find('span').text(text);
+    warningNotification.show();
+});
+window.Events.listen('error', function (text) {
+    errorNotification.find('span').text(text);
+    errorNotification.show();
 });
 
+// Notification hiding
+notifications.click(function () {
+    $(this).fadeOut(100);
+});
+
+// Chapter page list toggles
+$('.chapter-toggle').click(function (e) {
+    e.preventDefault();
+    $(this).toggleClass('open');
+    $(this).closest('.chapter').find('.inset-list').slideToggle(180);
+});
+
+// Back to top button
+$('#back-to-top').click(function() {
+     $('#header').smoothScrollTo();
+});
+let scrollTopShowing = false;
+let scrollTop = document.getElementById('back-to-top');
+let scrollTopBreakpoint = 1200;
+window.addEventListener('scroll', function() {
+    let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
+    if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
+        scrollTop.style.display = 'block';
+        scrollTopShowing = true;
+        setTimeout(() => {
+            scrollTop.style.opacity = 0.4;
+        }, 1);
+    } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
+        scrollTop.style.opacity = 0;
+        scrollTopShowing = false;
+        setTimeout(() => {
+            scrollTop.style.display = 'none';
+        }, 500);
+    }
+});
+
+// Common jQuery actions
+$('[data-action="expand-entity-list-details"]').click(function() {
+    $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
+});
+
+// Popup close
+$('.popup-close').click(function() {
+    $(this).closest('.overlay').fadeOut(240);
+});
+$('.overlay').click(function(event) {
+    if (!$(event.target).hasClass('overlay')) return;
+    $(this).fadeOut(240);
+});
+
+// Detect IE for css
+if(navigator.userAgent.indexOf('MSIE')!==-1
+    || navigator.appVersion.indexOf('Trident/') > 0
+    || navigator.userAgent.indexOf('Safari') !== -1){
+    $('body').addClass('flexbox-support');
+}
+
 // Page specific items
-require('./pages/page-show');
+import "./pages/page-show";
diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js
index 1fb8b915f..e1c0cfe8f 100644
--- a/resources/assets/js/pages/page-form.js
+++ b/resources/assets/js/pages/page-form.js
@@ -60,108 +60,108 @@ function registerEditorShortcuts(editor) {
     editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
 }
 
-var mceOptions = module.exports = {
-    selector: '#html-editor',
-    content_css: [
-        window.baseUrl('/css/styles.css'),
-        window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
-    ],
-    body_class: 'page-content',
-    relative_urls: false,
-    remove_script_host: false,
-    document_base_url: window.baseUrl('/'),
-    statusbar: false,
-    menubar: false,
-    paste_data_images: false,
-    extended_valid_elements: 'pre[*]',
-    automatic_uploads: false,
-    valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
-    plugins: "image table textcolor paste link fullscreen imagetools code customhr autosave lists",
-    imagetools_toolbar: 'imageoptions',
-    toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
-    content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
-    style_formats: [
-        {title: "Header Large", format: "h2"},
-        {title: "Header Medium", format: "h3"},
-        {title: "Header Small", format: "h4"},
-        {title: "Header Tiny", format: "h5"},
-        {title: "Paragraph", format: "p", exact: true, classes: ''},
-        {title: "Blockquote", format: "blockquote"},
-        {title: "Code Block", icon: "code", format: "pre"},
-        {title: "Inline Code", icon: "code", inline: "code"},
-        {title: "Callouts", items: [
-            {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
-            {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
-            {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
-            {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
-        ]}
-    ],
-    style_formats_merge: false,
-    formats: {
-        alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
-        aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
-        alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
-    },
-    file_browser_callback: function (field_name, url, type, win) {
+export default function() {
+    let settings = {
+        selector: '#html-editor',
+        content_css: [
+            window.baseUrl('/css/styles.css'),
+            window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
+        ],
+        body_class: 'page-content',
+        relative_urls: false,
+        remove_script_host: false,
+        document_base_url: window.baseUrl('/'),
+        statusbar: false,
+        menubar: false,
+        paste_data_images: false,
+        extended_valid_elements: 'pre[*]',
+        automatic_uploads: false,
+        valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
+        plugins: "image table textcolor paste link fullscreen imagetools code customhr autosave lists",
+        imagetools_toolbar: 'imageoptions',
+        toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
+        content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
+        style_formats: [
+            {title: "Header Large", format: "h2"},
+            {title: "Header Medium", format: "h3"},
+            {title: "Header Small", format: "h4"},
+            {title: "Header Tiny", format: "h5"},
+            {title: "Paragraph", format: "p", exact: true, classes: ''},
+            {title: "Blockquote", format: "blockquote"},
+            {title: "Code Block", icon: "code", format: "pre"},
+            {title: "Inline Code", icon: "code", inline: "code"},
+            {title: "Callouts", items: [
+                {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
+                {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
+                {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
+                {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
+            ]}
+        ],
+        style_formats_merge: false,
+        formats: {
+            alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
+            aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
+            alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
+        },
+        file_browser_callback: function (field_name, url, type, win) {
 
-        if (type === 'file') {
-            window.showEntityLinkSelector(function(entity) {
-                let originalField = win.document.getElementById(field_name);
-                originalField.value = entity.link;
-                $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
-            });
-        }
+            if (type === 'file') {
+                window.showEntityLinkSelector(function(entity) {
+                    let originalField = win.document.getElementById(field_name);
+                    originalField.value = entity.link;
+                    $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
+                });
+            }
 
-        if (type === 'image') {
-            // Show image manager
-            window.ImageManager.showExternal(function (image) {
+            if (type === 'image') {
+                // Show image manager
+                window.ImageManager.showExternal(function (image) {
 
-                // Set popover link input to image url then fire change event
-                // to ensure the new value sticks
-                win.document.getElementById(field_name).value = image.url;
-                if ("createEvent" in document) {
-                    let evt = document.createEvent("HTMLEvents");
-                    evt.initEvent("change", false, true);
-                    win.document.getElementById(field_name).dispatchEvent(evt);
-                } else {
-                    win.document.getElementById(field_name).fireEvent("onchange");
-                }
+                    // Set popover link input to image url then fire change event
+                    // to ensure the new value sticks
+                    win.document.getElementById(field_name).value = image.url;
+                    if ("createEvent" in document) {
+                        let evt = document.createEvent("HTMLEvents");
+                        evt.initEvent("change", false, true);
+                        win.document.getElementById(field_name).dispatchEvent(evt);
+                    } else {
+                        win.document.getElementById(field_name).fireEvent("onchange");
+                    }
 
-                // Replace the actively selected content with the linked image
-                let html = `<a href="${image.url}" target="_blank">`;
-                html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
-                html += '</a>';
-                win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
-            });
-        }
+                    // Replace the actively selected content with the linked image
+                    let html = `<a href="${image.url}" target="_blank">`;
+                    html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
+                    html += '</a>';
+                    win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
+                });
+            }
 
-    },
-    paste_preprocess: function (plugin, args) {
-        let content = args.content;
-        if (content.indexOf('<img src="file://') !== -1) {
-            args.content = '';
-        }
-    },
-    extraSetups: [],
-    setup: function (editor) {
+        },
+        paste_preprocess: function (plugin, args) {
+            let content = args.content;
+            if (content.indexOf('<img src="file://') !== -1) {
+                args.content = '';
+            }
+        },
+        extraSetups: [],
+        setup: function (editor) {
 
-        // Run additional setup actions
-        // Used by the angular side of things
-        for (let i = 0; i < mceOptions.extraSetups.length; i++) {
-            mceOptions.extraSetups[i](editor);
-        }
+            // Run additional setup actions
+            // Used by the angular side of things
+            for (let i = 0; i < settings.extraSetups.length; i++) {
+                settings.extraSetups[i](editor);
+            }
 
-        registerEditorShortcuts(editor);
+            registerEditorShortcuts(editor);
 
-        (function () {
-            var wrap;
+            let wrap;
 
             function hasTextContent(node) {
                 return node && !!( node.textContent || node.innerText );
             }
 
             editor.on('dragstart', function () {
-                var node = editor.selection.getNode();
+                let node = editor.selection.getNode();
 
                 if (node.nodeName !== 'IMG') return;
                 wrap = editor.dom.getParent(node, '.mceTemp');
@@ -172,7 +172,7 @@ var mceOptions = module.exports = {
             });
 
             editor.on('drop', function (event) {
-                var dom = editor.dom,
+                let dom = editor.dom,
                     rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
 
                 // Don't allow anything to be dropped in a captioned image.
@@ -190,26 +190,27 @@ var mceOptions = module.exports = {
 
                 wrap = null;
             });
-        })();
 
-        // Custom Image picker button
-        editor.addButton('image-insert', {
-            title: 'My title',
-            icon: 'image',
-            tooltip: 'Insert an image',
-            onclick: function () {
-                window.ImageManager.showExternal(function (image) {
-                    let html = `<a href="${image.url}" target="_blank">`;
-                    html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
-                    html += '</a>';
-                    editor.execCommand('mceInsertContent', false, html);
-                });
-            }
-        });
+            // Custom Image picker button
+            editor.addButton('image-insert', {
+                title: 'My title',
+                icon: 'image',
+                tooltip: 'Insert an image',
+                onclick: function () {
+                    window.ImageManager.showExternal(function (image) {
+                        let html = `<a href="${image.url}" target="_blank">`;
+                        html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
+                        html += '</a>';
+                        editor.execCommand('mceInsertContent', false, html);
+                    });
+                }
+            });
 
-        // Paste image-uploads
-        editor.on('paste', function(event) {
-            editorPaste(event, editor);
-        });
-    }
-};
\ No newline at end of file
+            // Paste image-uploads
+            editor.on('paste', function(event) {
+                editorPaste(event, editor);
+            });
+        }
+    };
+    return settings;
+}
\ No newline at end of file
diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js
index 41b92453f..0cdde790d 100644
--- a/resources/assets/js/pages/page-show.js
+++ b/resources/assets/js/pages/page-show.js
@@ -1,16 +1,13 @@
 "use strict";
 // Configure ZeroClipboard
-var zeroClipBoard = require('zeroclipboard');
-zeroClipBoard.config({
-    swfPath: window.baseUrl('/ZeroClipboard.swf')
-});
+import zeroClipBoard from "zeroclipboard";
 
-window.setupPageShow = module.exports = function (pageId) {
+export default window.setupPageShow = function (pageId) {
 
     // Set up pointer
-    var $pointer = $('#pointer').detach();
-    var $pointerInner = $pointer.children('div.pointer').first();
-    var isSelection = false;
+    let $pointer = $('#pointer').detach();
+    let $pointerInner = $pointer.children('div.pointer').first();
+    let isSelection = false;
 
     // Select all contents on input click
     $pointer.on('click', 'input', function (e) {
@@ -19,6 +16,9 @@ window.setupPageShow = module.exports = function (pageId) {
     });
 
     // Set up copy-to-clipboard
+    zeroClipBoard.config({
+        swfPath: window.baseUrl('/ZeroClipboard.swf')
+    });
     new zeroClipBoard($pointer.find('button').first()[0]);
 
     // Hide pointer when clicking away
@@ -31,11 +31,11 @@ window.setupPageShow = module.exports = function (pageId) {
     // Show pointer when selecting a single block of tagged content
     $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
         e.stopPropagation();
-        var selection = window.getSelection();
+        let selection = window.getSelection();
         if (selection.toString().length === 0) return;
 
         // Show pointer and set link
-        var $elem = $(this);
+        let $elem = $(this);
         let link = window.baseUrl('/link/' + pageId + '#' + $elem.attr('id'));
         if (link.indexOf('http') !== 0) link = window.location.protocol + "//" + window.location.host + link;
         $pointer.find('input').val(link);
@@ -44,9 +44,9 @@ window.setupPageShow = module.exports = function (pageId) {
         $pointer.show();
 
         // Set pointer to sit near mouse-up position
-        var pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
+        let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
         if (pointerLeftOffset < 0) pointerLeftOffset = 0;
-        var pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
+        let pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
         $pointerInner.css('left', pointerLeftOffsetPercent + '%');
 
         isSelection = true;
@@ -57,7 +57,7 @@ window.setupPageShow = module.exports = function (pageId) {
 
     // Go to, and highlight if necessary, the specified text.
     function goToText(text) {
-        var idElem = $('.page-content #' + text).first();
+        let idElem = $('.page-content #' + text).first();
         if (idElem.length !== 0) {
             idElem.smoothScrollTo();
             idElem.css('background-color', 'rgba(244, 249, 54, 0.25)');
@@ -68,19 +68,19 @@ window.setupPageShow = module.exports = function (pageId) {
 
     // Check the hash on load
     if (window.location.hash) {
-        var text = window.location.hash.replace(/\%20/g, ' ').substr(1);
+        let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
         goToText(text);
     }
 
     // Make the book-tree sidebar stick in view on scroll
-    var $window = $(window);
-    var $bookTree = $(".book-tree");
-    var $bookTreeParent = $bookTree.parent();
+    let $window = $(window);
+    let $bookTree = $(".book-tree");
+    let $bookTreeParent = $bookTree.parent();
     // Check the page is scrollable and the content is taller than the tree
-    var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
+    let pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
     // Get current tree's width and header height
-    var headerHeight = $("#header").height() + $(".toolbar").height();
-    var isFixed = $window.scrollTop() > headerHeight;
+    let headerHeight = $("#header").height() + $(".toolbar").height();
+    let isFixed = $window.scrollTop() > headerHeight;
     // Function to fix the tree as a sidebar
     function stickTree() {
         $bookTree.width($bookTreeParent.width() + 15);
@@ -95,7 +95,7 @@ window.setupPageShow = module.exports = function (pageId) {
     }
     // Checks if the tree stickiness state should change
     function checkTreeStickiness(skipCheck) {
-        var shouldBeFixed = $window.scrollTop() > headerHeight;
+        let shouldBeFixed = $window.scrollTop() > headerHeight;
         if (shouldBeFixed && (!isFixed || skipCheck)) {
             stickTree();
         } else if (!shouldBeFixed && (isFixed || skipCheck)) {
diff --git a/resources/assets/js/translations.js b/resources/assets/js/translations.js
new file mode 100644
index 000000000..306c696b6
--- /dev/null
+++ b/resources/assets/js/translations.js
@@ -0,0 +1,47 @@
+/**
+ *  Translation Manager
+ *  Handles the JavaScript side of translating strings
+ *  in a way which fits with Laravel.
+ */
+class Translator {
+
+    /**
+     * Create an instance, Passing in the required translations
+     * @param translations
+     */
+    constructor(translations) {
+        this.store = translations;
+    }
+
+    /**
+     * Get a translation, Same format as laravel's 'trans' helper
+     * @param key
+     * @param replacements
+     * @returns {*}
+     */
+    get(key, replacements) {
+        let splitKey = key.split('.');
+        let value = splitKey.reduce((a, b) => {
+            return a != undefined ? a[b] : a;
+        }, this.store);
+
+        if (value === undefined) {
+            console.log(`Translation with key "${key}" does not exist`);
+            value = key;
+        }
+
+        if (replacements === undefined) return value;
+
+        let replaceMatches = value.match(/:([\S]+)/g);
+        if (replaceMatches === null) return value;
+        replaceMatches.forEach(match => {
+            let key = match.substring(1);
+            if (typeof replacements[key] === 'undefined') return;
+            value = value.replace(match, replacements[key]);
+        });
+        return value;
+    }
+
+}
+
+export default Translator
diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss
index 2f9051a52..c8fd8bcfa 100644
--- a/resources/assets/sass/_components.scss
+++ b/resources/assets/sass/_components.scss
@@ -465,4 +465,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
       border-bottom-width: 3px;
     }
   }
+}
+
+.image-picker .none {
+  display: none;
 }
\ No newline at end of file
diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss
index 4e643dcda..7e6b800d2 100644
--- a/resources/assets/sass/_forms.scss
+++ b/resources/assets/sass/_forms.scss
@@ -33,7 +33,7 @@
   position: relative;
   z-index: 5;
   textarea {
-    font-family: 'Roboto Mono';
+    font-family: 'Roboto Mono', monospace;
     font-style: normal;
     font-weight: 400;
     padding: $-xs $-m;
@@ -55,6 +55,7 @@
     display: flex;
     flex-direction: column;
     border: 1px solid #DDD;
+    width: 50%;
   }
   .markdown-display {
     padding: 0 $-m 0;
@@ -68,7 +69,7 @@
 .editor-toolbar {
   width: 100%;
   padding: $-xs $-m;
-  font-family: 'Roboto Mono';
+  font-family: 'Roboto Mono', monospace;
   font-size: 11px;
   line-height: 1.6;
   border-bottom: 1px solid #DDD;
@@ -267,9 +268,4 @@ input.outline {
 
 .image-picker img {
   background-color: #BBB;
-}
-
-div[toggle-switch] {
-  height: 18px;
-  width: 150px;
 }
\ No newline at end of file
diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss
index e98e5bfcd..6acc47468 100644
--- a/resources/assets/sass/_lists.scss
+++ b/resources/assets/sass/_lists.scss
@@ -322,6 +322,9 @@ ul.pagination {
     font-size: 0.75em;
     margin-top: $-xs;
   }
+  .text-muted p.text-muted {
+    margin-top: 0;
+  }
   .page.draft .text-page {
     color: $color-page-draft;
   }
diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss
index 37c61159d..21553b839 100644
--- a/resources/assets/sass/_tables.scss
+++ b/resources/assets/sass/_tables.scss
@@ -35,6 +35,12 @@ table.table {
   tr:hover {
     background-color: #EEE;
   }
+  .text-right {
+    text-align: right;
+  }
+  .text-center {
+    text-align: center;
+  }
 }
 
 table.no-style {
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index 9bad2e83d..dd2f32e1c 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -109,6 +109,9 @@ em, i, .italic {
 small, p.small, span.small, .text-small {
   font-size: 0.8em;
   color: lighten($text-dark, 20%);
+  small, p.small, span.small, .text-small {
+    font-size: 1em;
+  }
 }
 
 sup, .superscript {
diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php
index 183480faa..0017acd1d 100644
--- a/resources/lang/de/settings.php
+++ b/resources/lang/de/settings.php
@@ -16,7 +16,7 @@ return [
     'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
     'app_name_header' => 'Anwendungsname im Header anzeigen?',
     'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
-    'app_secure_images' => 'Erh&oml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
+    'app_secure_images' => 'Erh&ouml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
     'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
     'app_editor' => 'Seiteneditor',
     'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php
index ffdb1cf45..b734828fc 100644
--- a/resources/lang/en/auth.php
+++ b/resources/lang/en/auth.php
@@ -14,7 +14,49 @@ return [
     'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
 
     /**
-     * Email Confirmation Text
+     * Login & Register
+     */
+    'sign_up' => 'Sign up',
+    'log_in' => 'Log in',
+    'logout' => 'Logout',
+
+    'name' => 'Name',
+    'username' => 'Username',
+    'email' => 'Email',
+    'password' => 'Password',
+    'password_confirm' => 'Confirm Password',
+    'password_hint' => 'Must be over 5 characters',
+    'forgot_password' => 'Forgot Password?',
+    'remember_me' => 'Remember Me',
+    'ldap_email_hint' => 'Please enter an email to use for this account.',
+    'create_account' => 'Create Account',
+    'social_login' => 'Social Login',
+    'social_registration' => 'Social Registration',
+    'social_registration_text' => 'Register and sign in using another service.',
+
+    'register_thanks' => 'Thanks for registering!',
+    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',
+    'registrations_disabled' => 'Registrations are currently disabled',
+    'registration_email_domain_invalid' => 'That email domain does not have access to this application',
+    'register_success' => 'Thanks for signing up! You are now registered and signed in.',
+
+
+    /**
+     * Password Reset
+     */
+    'reset_password' => 'Reset Password',
+    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',
+    'reset_password_send_button' => 'Send Reset Link',
+    'reset_password_sent_success' => 'A password reset link has been sent to :email.',
+    'reset_password_success' => 'Your password has been successfully reset.',
+
+    'email_reset_subject' => 'Reset your :appName password',
+    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',
+    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',
+
+
+    /**
+     * Email Confirmation
      */
     'email_confirm_subject' => 'Confirm your email on :appName',
     'email_confirm_greeting' => 'Thanks for joining :appName!',
@@ -23,4 +65,10 @@ return [
     'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
     'email_confirm_success' => 'Your email has been confirmed!',
     'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+
+    'email_not_confirmed' => 'Email Address Not Confirmed',
+    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
+    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
+    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
+    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
 ];
\ No newline at end of file
diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php
new file mode 100644
index 000000000..31ef42e97
--- /dev/null
+++ b/resources/lang/en/common.php
@@ -0,0 +1,58 @@
+<?php
+return [
+
+    /**
+     * Buttons
+     */
+    'cancel' => 'Cancel',
+    'confirm' => 'Confirm',
+    'back' => 'Back',
+    'save' => 'Save',
+    'continue' => 'Continue',
+    'select' => 'Select',
+
+    /**
+     * Form Labels
+     */
+    'name' => 'Name',
+    'description' => 'Description',
+    'role' => 'Role',
+
+    /**
+     * Actions
+     */
+    'actions' => 'Actions',
+    'view' => 'View',
+    'create' => 'Create',
+    'update' => 'Update',
+    'edit' => 'Edit',
+    'sort' => 'Sort',
+    'move' => 'Move',
+    'delete' => 'Delete',
+    'search' => 'Search',
+    'search_clear' => 'Clear Search',
+    'reset' => 'Reset',
+    'remove' => 'Remove',
+
+
+    /**
+     * Misc
+     */
+    'deleted_user' => 'Deleted User',
+    'no_activity' => 'No activity to show',
+    'no_items' => 'No items available',
+    'back_to_top' => 'Back to top',
+    'toggle_details' => 'Toggle Details',
+
+    /**
+     * Header
+     */
+    'view_profile' => 'View Profile',
+    'edit_profile' => 'Edit Profile',
+
+    /**
+     * Email Content
+     */
+    'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
+    'email_rights' => 'All rights reserved',
+];
\ No newline at end of file
diff --git a/resources/lang/en/components.php b/resources/lang/en/components.php
new file mode 100644
index 000000000..b9108702a
--- /dev/null
+++ b/resources/lang/en/components.php
@@ -0,0 +1,24 @@
+<?php
+return [
+
+    /**
+     * Image Manager
+     */
+    'image_select' => 'Image Select',
+    'image_all' => 'All',
+    'image_all_title' => 'View all images',
+    'image_book_title' => 'View images uploaded to this book',
+    'image_page_title' => 'View images uploaded to this page',
+    'image_search_hint' => 'Search by image name',
+    'image_uploaded' => 'Uploaded :uploadedDate',
+    'image_load_more' => 'Load More',
+    'image_image_name' => 'Image Name',
+    'image_delete_confirm' => 'This image is used in the pages below, Click delete again to confirm you want to delete this image.',
+    'image_select_image' => 'Select Image',
+    'image_dropzone' => 'Drop images or click here to upload',
+    'images_deleted' => 'Images Deleted',
+    'image_preview' => 'Image Preview',
+    'image_upload_success' => 'Image uploaded successfully',
+    'image_update_success' => 'Image details successfully updated',
+    'image_delete_success' => 'Image successfully deleted'
+];
\ No newline at end of file
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
new file mode 100644
index 000000000..033d9614e
--- /dev/null
+++ b/resources/lang/en/entities.php
@@ -0,0 +1,225 @@
+<?php
+return [
+
+    /**
+     * Shared
+     */
+    'recently_created' => 'Recently Created',
+    'recently_created_pages' => 'Recently Created Pages',
+    'recently_updated_pages' => 'Recently Updated Pages',
+    'recently_created_chapters' => 'Recently Created Chapters',
+    'recently_created_books' => 'Recently Created Books',
+    'recently_update' => 'Recently Updated',
+    'recently_viewed' => 'Recently Viewed',
+    'recent_activity' => 'Recent Activity',
+    'create_now' => 'Create one now',
+    'revisions' => 'Revisions',
+    'meta_created' => 'Created :timeLength',
+    'meta_created_name' => 'Created :timeLength by :user',
+    'meta_updated' => 'Updated :timeLength',
+    'meta_updated_name' => 'Updated :timeLength by :user',
+    'x_pages' => ':count Pages',
+    'entity_select' => 'Entity Select',
+    'images' => 'Images',
+    'my_recent_drafts' => 'My Recent Drafts',
+    'my_recently_viewed' => 'My Recently Viewed',
+    'no_pages_viewed' => 'You have not viewed any pages',
+    'no_pages_recently_created' => 'No pages have been recently created',
+    'no_pages_recently_updated' => 'No pages have been recently updated',
+
+    /**
+     * Permissions and restrictions
+     */
+    'permissions' => 'Permissions',
+    'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
+    'permissions_enable' => 'Enable Custom Permissions',
+    'permissions_save' => 'Save Permissions',
+
+    /**
+     * Search
+     */
+    'search_results' => 'Search Results',
+    'search_results_page' => 'Page Search Results',
+    'search_results_chapter' => 'Chapter Search Results',
+    'search_results_book' => 'Book Search Results',
+    'search_clear' => 'Clear Search',
+    'search_view_pages' => 'View all matches pages',
+    'search_view_chapters' => 'View all matches chapters',
+    'search_view_books' => 'View all matches books',
+    'search_no_pages' => 'No pages matched this search',
+    'search_for_term' => 'Search for :term',
+    'search_page_for_term' => 'Page search for :term',
+    'search_chapter_for_term' => 'Chapter search for :term',
+    'search_book_for_term' => 'Books search for :term',
+
+    /**
+     * Books
+     */
+    'book' => 'Book',
+    'books' => 'Books',
+    'books_empty' => 'No books have been created',
+    'books_popular' => 'Popular Books',
+    'books_recent' => 'Recent Books',
+    'books_popular_empty' => 'The most popular books will appear here.',
+    'books_create' => 'Create New Book',
+    'books_delete' => 'Delete Book',
+    'books_delete_named' => 'Delete Book :bookName',
+    'books_delete_explain' => 'This will delete the book with the name \':bookName\', All pages and chapters will be removed.',
+    'books_delete_confirmation' => 'Are you sure you want to delete this book?',
+    'books_edit' => 'Edit Book',
+    'books_edit_named' => 'Edit Book :bookName',
+    'books_form_book_name' => 'Book Name',
+    'books_save' => 'Save Book',
+    'books_permissions' => 'Book Permissions',
+    'books_permissions_updated' => 'Book Permissions Updated',
+    'books_empty_contents' => 'No pages or chapters have been created for this book.',
+    'books_empty_create_page' => 'Create a new page',
+    'books_empty_or' => 'or',
+    'books_empty_sort_current_book' => 'Sort the current book',
+    'books_empty_add_chapter' => 'Add a chapter',
+    'books_permissions_active' => 'Book Permissions Active',
+    'books_search_this' => 'Search this book',
+    'books_navigation' => 'Book Navigation',
+    'books_sort' => 'Sort Book Contents',
+    'books_sort_named' => 'Sort Book :bookName',
+    'books_sort_show_other' => 'Show Other Books',
+    'books_sort_save' => 'Save New Order',
+
+    /**
+     * Chapters
+     */
+    'chapter' => 'Chapter',
+    'chapters_popular' => 'Popular Chapters',
+    'chapters_new' => 'New Chapter',
+    'chapters_create' => 'Create New Chapter',
+    'chapters_delete' => 'Delete Chapter',
+    'chapters_delete_named' => 'Delete Chapter :chapterName',
+    'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\', All pages will be removed
+        and added directly to the parent book.',
+    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
+    'chapters_edit' => 'Edit Chapter',
+    'chapters_edit_named' => 'Edit Chapter :chapterName',
+    'chapters_save' => 'Save Chapter',
+    'chapters_move' => 'Move Chapter',
+    'chapters_move_named' => 'Move Chapter :chapterName',
+    'chapter_move_success' => 'Chapter moved to :bookName',
+    'chapters_permissions' => 'Chapter Permissions',
+    'chapters_empty' => 'No pages are currently in this chapter.',
+    'chapters_permissions_active' => 'Chapter Permissions Active',
+    'chapters_permissions_success' => 'Chapter Permissions Updated',
+
+    /**
+     * Pages
+     */
+    'page' => 'Page',
+    'pages' => 'Pages',
+    'pages_popular' => 'Popular Pages',
+    'pages_new' => 'New Page',
+    'pages_attachments' => 'Attachments',
+    'pages_navigation' => 'Page Navigation',
+    'pages_delete' => 'Delete Page',
+    'pages_delete_named' => 'Delete Page :pageName',
+    'pages_delete_draft_named' => 'Delete Draft Page :pageName',
+    'pages_delete_draft' => 'Delete Draft Page',
+    'pages_delete_success' => 'Page deleted',
+    'pages_delete_draft_success' => 'Draft page deleted',
+    'pages_delete_confirm' => 'Are you sure you want to delete this page?',
+    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
+    'pages_editing_named' => 'Editing Page :pageName',
+    'pages_edit_toggle_header' => 'Toggle header',
+    'pages_edit_save_draft' => 'Save Draft',
+    'pages_edit_draft' => 'Edit Page Draft',
+    'pages_editing_draft' => 'Editing Draft',
+    'pages_editing_page' => 'Editing Page',
+    'pages_edit_draft_save_at' => 'Draft saved at ',
+    'pages_edit_delete_draft' => 'Delete Draft',
+    'pages_edit_discard_draft' => 'Discard Draft',
+    'pages_edit_set_changelog' => 'Set Changelog',
+    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
+    'pages_edit_enter_changelog' => 'Enter Changelog',
+    'pages_save' => 'Save Page',
+    'pages_title' => 'Page Title',
+    'pages_name' => 'Page Name',
+    'pages_md_editor' => 'Editor',
+    'pages_md_preview' => 'Preview',
+    'pages_md_insert_image' => 'Insert Image',
+    'pages_md_insert_link' => 'Insert Entity Link',
+    'pages_not_in_chapter' => 'Page is not in a chapter',
+    'pages_move' => 'Move Page',
+    'pages_move_success' => 'Page moved to ":parentName"',
+    'pages_permissions' => 'Page Permissions',
+    'pages_permissions_success' => 'Page permissions updated',
+    'pages_revisions' => 'Page Revisions',
+    'pages_revisions_named' => 'Page Revisions for :pageName',
+    'pages_revision_named' => 'Page Revision for :pageName',
+    'pages_revisions_created_by' => 'Created By',
+    'pages_revisions_date' => 'Revision Date',
+    'pages_revisions_changelog' => 'Changelog',
+    'pages_revisions_changes' => 'Changes',
+    'pages_revisions_current' => 'Current Version',
+    'pages_revisions_preview' => 'Preview',
+    'pages_revisions_restore' => 'Restore',
+    'pages_revisions_none' => 'This page has no revisions',
+    'pages_export' => 'Export',
+    'pages_export_html' => 'Contained Web File',
+    'pages_export_pdf' => 'PDF File',
+    'pages_export_text' => 'Plain Text File',
+    'pages_copy_link' => 'Copy Link',
+    'pages_permissions_active' => 'Page Permissions Active',
+    'pages_initial_revision' => 'Initial publish',
+    'pages_initial_name' => 'New Page',
+    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
+    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count users have started editing this page',
+        'start_b' => ':userName has started editing this page',
+        'time_a' => 'since the pages was last updated',
+        'time_b' => 'in the last :minCount minutes',
+        'message' => ':start :time. Take care not to overwrite each other\'s updates!',
+    ],
+    'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
+
+    /**
+     * Editor sidebar
+     */
+    'page_tags' => 'Page Tags',
+    'tag' => 'Tag',
+    'tags' =>  '',
+    'tag_value' => 'Tag Value (Optional)',
+    'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
+    'tags_add' => 'Add another tag',
+    'attachments' => 'Attachments',
+    'attachments_explain' => 'Upload some files or attach some link to display on your page. These are visible in the page sidebar.',
+    'attachments_explain_instant_save' => 'Changes here are saved instantly.',
+    'attachments_items' => 'Attached Items',
+    'attachments_upload' => 'Upload File',
+    'attachments_link' => 'Attach Link',
+    'attachments_set_link' => 'Set Link',
+    'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.',
+    'attachments_dropzone' => 'Drop files or click here to attach a file',
+    'attachments_no_files' => 'No files have been uploaded',
+    'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',
+    'attachments_link_name' => 'Link Name',
+    'attachment_link' => 'Attachment link',
+    'attachments_link_url' => 'Link to file',
+    'attachments_link_url_hint' => 'Url of site or file',
+    'attach' => 'Attach',
+    'attachments_edit_file' => 'Edit File',
+    'attachments_edit_file_name' => 'File Name',
+    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
+    'attachments_order_updated' => 'Attachment order updated',
+    'attachments_updated_success' => 'Attachment details updated',
+    'attachments_deleted' => 'Attachment deleted',
+    'attachments_file_uploaded' => 'File successfully uploaded',
+    'attachments_file_updated' => 'File successfully updated',
+    'attachments_link_attached' => 'Link successfully attached to page',
+
+    /**
+     * Profile View
+     */
+    'profile_user_for_x' => 'User for :time',
+    'profile_created_content' => 'Created Content',
+    'profile_not_created_pages' => ':userName has not created any pages',
+    'profile_not_created_chapters' => ':userName has not created any chapters',
+    'profile_not_created_books' => ':userName has not created any books',
+];
\ No newline at end of file
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
index b1a252bf3..c4578a37a 100644
--- a/resources/lang/en/errors.php
+++ b/resources/lang/en/errors.php
@@ -6,7 +6,65 @@ return [
      * Error text strings.
      */
 
-    // Pages
+    // Permissions
     'permission' => 'You do not have permission to access the requested page.',
-    'permissionJson' => 'You do not have permission to perform the requested action.'
+    'permissionJson' => 'You do not have permission to perform the requested action.',
+
+    // Auth
+    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
+    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
+    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
+    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
+    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
+    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
+    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
+    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
+    'social_no_action_defined' => 'No action defined',
+    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
+    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
+    'social_account_existing' => 'This :socialAccount is already attached to your profile.',
+    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
+    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',
+    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
+    'social_driver_not_found' => 'Social driver not found',
+    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+
+    // System
+    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
+    'cannot_get_image_from_url' => 'Cannot get image from :url',
+    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
+    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
+    'image_upload_error' => 'An error occurred uploading the image',
+
+    // Attachments
+    'attachment_page_mismatch' => 'Page mismatch during attachment update',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
+
+    // Entities
+    'entity_not_found' => 'Entity not found',
+    'book_not_found' => 'Book not found',
+    'page_not_found' => 'Page not found',
+    'chapter_not_found' => 'Chapter not found',
+    'selected_book_not_found' => 'The selected book was not found',
+    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
+    'guests_cannot_save_drafts' => 'Guests cannot save drafts',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
+    'users_cannot_delete_guest' => 'You cannot delete the guest user',
+
+    // Roles
+    'role_cannot_be_edited' => 'This role cannot be edited',
+    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
+    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
+
+    // Error pages
+    '404_page_not_found' => 'Page Not Found',
+    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
+    'return_home' => 'Return to home',
+    'error_occurred' => 'An Error Occurred',
+    'app_down' => ':appName is down right now',
+    'back_soon' => 'It will be back up soon.',
 ];
\ No newline at end of file
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index 1b0bcad33..e61df19d9 100644
--- a/resources/lang/en/settings.php
+++ b/resources/lang/en/settings.php
@@ -10,7 +10,12 @@ return [
     
     'settings' => 'Settings',
     'settings_save' => 'Save Settings',
-    
+    'settings_save_success' => 'Settings saved',
+
+    /**
+     * App settings
+     */
+
     'app_settings' => 'App Settings',
     'app_name' => 'Application name',
     'app_name_desc' => 'This name is shown in the header and any emails.',
@@ -27,6 +32,10 @@ return [
     'app_primary_color' => 'Application primary color',
     'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
 
+    /**
+     * Registration settings
+     */
+
     'reg_settings' => 'Registration Settings',
     'reg_allow' => 'Allow registration?',
     'reg_default_role' => 'Default user role after registration',
@@ -36,4 +45,96 @@ return [
     'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
     'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
 
-];
\ No newline at end of file
+    /**
+     * Role settings
+     */
+
+    'roles' => 'Roles',
+    'role_user_roles' => 'User Roles',
+    'role_create' => 'Create New Role',
+    'role_create_success' => 'Role successfully created',
+    'role_delete' => 'Delete Role',
+    'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
+    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
+    'role_delete_no_migration' => "Don't migrate users",
+    'role_delete_sure' => 'Are you sure you want to delete this role?',
+    'role_delete_success' => 'Role successfully deleted',
+    'role_edit' => 'Edit Role',
+    'role_details' => 'Role Details',
+    'role_name' => 'Role Name',
+    'role_desc' => 'Short Description of Role',
+    'role_system' => 'System Permissions',
+    'role_manage_users' => 'Manage users',
+    'role_manage_roles' => 'Manage roles & role permissions',
+    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
+    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
+    'role_manage_settings' => 'Manage app settings',
+    'role_asset' => 'Asset Permissions',
+    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
+    'role_all' => 'All',
+    'role_own' => 'Own',
+    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
+    'role_save' => 'Save Role',
+    'role_update_success' => 'Role successfully updated',
+    'role_users' => 'Users in this role',
+    'role_users_none' => 'No users are currently assigned to this role',
+
+    /**
+     * Users
+     */
+
+    'users' => 'Users',
+    'user_profile' => 'User Profile',
+    'users_add_new' => 'Add New User',
+    'users_search' => 'Search Users',
+    'users_role' => 'User Roles',
+    'users_external_auth_id' => 'External Authentication ID',
+    'users_password_warning' => 'Only fill the below if you would like to change your password:',
+    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
+    'users_delete' => 'Delete User',
+    'users_delete_named' => 'Delete ser :userName',
+    'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
+    'users_delete_confirm' => 'Are you sure you want to delete this user?',
+    'users_delete_success' => 'Users successfully removed',
+    'users_edit' => 'Edit User',
+    'users_edit_profile' => 'Edit Profile',
+    'users_edit_success' => 'User successfully updated',
+    'users_avatar' => 'User Avatar',
+    'users_avatar_desc' => 'This image should be approx 256px square.',
+    'users_social_accounts' => 'Social Accounts',
+    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
+    'users_social_connect' => 'Connect Account',
+    'users_social_disconnect' => 'Disconnect Account',
+    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
+    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
+];
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php
index 20acc9a68..b75af7485 100644
--- a/resources/lang/en/validation.php
+++ b/resources/lang/en/validation.php
@@ -87,8 +87,8 @@ return [
     */
 
     'custom' => [
-        'attribute-name' => [
-            'rule-name' => 'custom-message',
+        'password-confirm' => [
+            'required_with' => 'Password confirmation required',
         ],
     ],
 
diff --git a/resources/views/auth/forms/login/ldap.blade.php b/resources/views/auth/forms/login/ldap.blade.php
index 5230d43ca..b52b5f13e 100644
--- a/resources/views/auth/forms/login/ldap.blade.php
+++ b/resources/views/auth/forms/login/ldap.blade.php
@@ -1,19 +1,19 @@
 <div class="form-group">
-    <label for="username">Username</label>
+    <label for="username">{{ trans('auth.username') }}</label>
     @include('form/text', ['name' => 'username', 'tabindex' => 1])
 </div>
 
 @if(session('request-email', false) === true)
     <div class="form-group">
-        <label for="email">Email</label>
+        <label for="email">{{ trans('auth.email') }}</label>
         @include('form/text', ['name' => 'email', 'tabindex' => 1])
         <span class="text-neg">
-            Please enter an email to use for this account.
+            {{ trans('auth.ldap_email_hint') }}
         </span>
     </div>
 @endif
 
 <div class="form-group">
-    <label for="password">Password</label>
+    <label for="password">{{ trans('auth.password') }}</label>
     @include('form/password', ['name' => 'password', 'tabindex' => 2])
 </div>
\ No newline at end of file
diff --git a/resources/views/auth/forms/login/standard.blade.php b/resources/views/auth/forms/login/standard.blade.php
index abefd21a1..4ea1f35ba 100644
--- a/resources/views/auth/forms/login/standard.blade.php
+++ b/resources/views/auth/forms/login/standard.blade.php
@@ -1,10 +1,10 @@
 <div class="form-group">
-    <label for="email">Email</label>
+    <label for="email">{{ trans('auth.email') }}</label>
     @include('form/text', ['name' => 'email', 'tabindex' => 1])
 </div>
 
 <div class="form-group">
-    <label for="password">Password</label>
+    <label for="password">{{ trans('auth.password') }}</label>
     @include('form/password', ['name' => 'password', 'tabindex' => 2])
-    <span class="block small"><a href="{{ baseUrl('/password/email') }}">Forgot Password?</a></span>
+    <span class="block small"><a href="{{ baseUrl('/password/email') }}">{{ trans('auth.forgot_password') }}</a></span>
 </div>
\ No newline at end of file
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index 4fa97c1d5..928565156 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -2,7 +2,7 @@
 
 @section('header-buttons')
     @if(setting('registration-enabled', false))
-        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
+        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
     @endif
 @stop
 
@@ -10,7 +10,7 @@
 
     <div class="text-center">
         <div class="center-box">
-            <h1>Log In</h1>
+            <h1>{{ title_case(trans('auth.log_in')) }}</h1>
 
             <form action="{{ baseUrl("/login") }}" method="POST" id="login-form">
                 {!! csrf_field() !!}
@@ -19,25 +19,25 @@
                 @include('auth/forms/login/' . $authMethod)
 
                 <div class="form-group">
-                    <label for="remember" class="inline">Remember Me</label>
+                    <label for="remember" class="inline">{{ trans('auth.remember_me') }}</label>
                     <input type="checkbox" id="remember" name="remember"  class="toggle-switch-checkbox">
                     <label for="remember" class="toggle-switch"></label>
                 </div>
 
 
                 <div class="from-group">
-                    <button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> Sign In</button>
+                    <button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> {{ title_case(trans('auth.log_in')) }}</button>
                 </div>
             </form>
 
             @if(count($socialDrivers) > 0)
                 <hr class="margin-top">
-                <h3 class="text-muted">Social Login</h3>
+                <h3 class="text-muted">{{ trans('auth.social_login') }}</h3>
                 @if(isset($socialDrivers['google']))
-                    <a href="{{ baseUrl("/login/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
+                    <a id="social-login-google" href="{{ baseUrl("/login/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
                 @endif
                 @if(isset($socialDrivers['github']))
-                    <a href="{{ baseUrl("/login/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a>
+                    <a id="social-login-github" href="{{ baseUrl("/login/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a>
                 @endif
             @endif
         </div>
diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php
index 115785ab2..07bd2c383 100644
--- a/resources/views/auth/passwords/email.blade.php
+++ b/resources/views/auth/passwords/email.blade.php
@@ -1,9 +1,9 @@
 @extends('public')
 
 @section('header-buttons')
-    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
+    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
     @if(setting('registration-enabled'))
-        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
+        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
     @endif
 @stop
 
@@ -12,20 +12,20 @@
 
     <div class="text-center">
         <div class="center-box text-left">
-            <h1>Reset Password</h1>
+            <h1>{{ trans('auth.reset_password') }}</h1>
 
-            <p class="muted small">Enter your email below and you will be sent an email with a password reset link.</p>
+            <p class="muted small">{{ trans('auth.reset_password_send_instructions') }}</p>
 
             <form action="{{ baseUrl("/password/email") }}" method="POST">
                 {!! csrf_field() !!}
 
                 <div class="form-group">
-                    <label for="email">Email</label>
+                    <label for="email">{{ trans('auth.email') }}</label>
                     @include('form/text', ['name' => 'email'])
                 </div>
 
                 <div class="from-group">
-                    <button class="button block pos">Send Reset Link</button>
+                    <button class="button block pos">{{ trans('auth.reset_password_send_button') }}</button>
                 </div>
             </form>
         </div>
diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php
index 612b50ff8..a463eef45 100644
--- a/resources/views/auth/passwords/reset.blade.php
+++ b/resources/views/auth/passwords/reset.blade.php
@@ -1,9 +1,9 @@
 @extends('public')
 
 @section('header-buttons')
-    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
+    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
     @if(setting('registration-enabled'))
-        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a>
+        <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
     @endif
 @stop
 
@@ -14,29 +14,29 @@
 
     <div class="text-center">
         <div class="center-box text-left">
-            <h1>Reset Password</h1>
+            <h1>{{ trans('auth.reset_password') }}</h1>
 
             <form action="{{ baseUrl("/password/reset") }}" method="POST">
                 {!! csrf_field() !!}
                 <input type="hidden" name="token" value="{{ $token }}">
 
                 <div class="form-group">
-                    <label for="email">Email</label>
+                    <label for="email">{{ trans('auth.email') }}</label>
                     @include('form/text', ['name' => 'email'])
                 </div>
 
                 <div class="form-group">
-                    <label for="password">Password</label>
+                    <label for="password">{{ trans('auth.password') }}</label>
                     @include('form/password', ['name' => 'password'])
                 </div>
 
                 <div class="form-group">
-                    <label for="password_confirmation">Confirm Password</label>
+                    <label for="password_confirmation">{{ trans('auth.password_confirm') }}</label>
                     @include('form/password', ['name' => 'password_confirmation'])
                 </div>
 
                 <div class="from-group">
-                    <button class="button block pos">Reset Password</button>
+                    <button class="button block pos">{{ trans('auth.reset_password') }}</button>
                 </div>
             </form>
         </div>
diff --git a/resources/views/auth/register-confirm.blade.php b/resources/views/auth/register-confirm.blade.php
index 97fd65ab5..364df9266 100644
--- a/resources/views/auth/register-confirm.blade.php
+++ b/resources/views/auth/register-confirm.blade.php
@@ -2,7 +2,7 @@
 
 @section('header-buttons')
     @if(!$signedIn)
-        <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
+        <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
     @endif
 @stop
 
@@ -10,10 +10,9 @@
 
     <div class="text-center">
         <div class="center-box">
-            <h2>Thanks for registering!</h2>
-            <p>Please check your email and click the confirmation button to access {{ setting('app-name', 'BookStack') }}.</p>
+            <h2>{{ trans('auth.register_thanks') }}</h2>
+            <p>{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}</p>
         </div>
     </div>
 
-
 @stop
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php
index 8ae5fcf50..7a119ddba 100644
--- a/resources/views/auth/register.blade.php
+++ b/resources/views/auth/register.blade.php
@@ -1,42 +1,42 @@
 @extends('public')
 
 @section('header-buttons')
-    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a>
+    <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
 @stop
 
 @section('content')
 
     <div class="text-center">
         <div class="center-box">
-            <h1>Sign Up</h1>
+            <h1>{{ title_case(trans('auth.sign_up')) }}</h1>
 
             <form action="{{ baseUrl("/register") }}" method="POST">
                 {!! csrf_field() !!}
 
                 <div class="form-group">
-                    <label for="email">Name</label>
+                    <label for="email">{{ trans('auth.name') }}</label>
                     @include('form/text', ['name' => 'name'])
                 </div>
 
                 <div class="form-group">
-                    <label for="email">Email</label>
+                    <label for="email">{{ trans('auth.email') }}</label>
                     @include('form/text', ['name' => 'email'])
                 </div>
 
                 <div class="form-group">
-                    <label for="password">Password</label>
-                    @include('form/password', ['name' => 'password', 'placeholder' => 'Must be over 5 characters'])
+                    <label for="password">{{ trans('auth.password') }}</label>
+                    @include('form/password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
                 </div>
 
                 <div class="from-group">
-                    <button class="button block pos">Create Account</button>
+                    <button class="button block pos">{{ trans('auth.create_account') }}</button>
                 </div>
             </form>
 
             @if(count($socialDrivers) > 0)
                 <hr class="margin-top">
-                <h3 class="text-muted">Social Registration</h3>
-                <p class="text-small">Register and sign in using another service.</p>
+                <h3 class="text-muted">{{ trans('auth.social_registration') }}</h3>
+                <p class="text-small">{{ trans('auth.social_registration_text') }}</p>
                 @if(isset($socialDrivers['google']))
                     <a href="{{ baseUrl("/register/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
                 @endif
diff --git a/resources/views/auth/user-unconfirmed.blade.php b/resources/views/auth/user-unconfirmed.blade.php
index 08178e891..13567b412 100644
--- a/resources/views/auth/user-unconfirmed.blade.php
+++ b/resources/views/auth/user-unconfirmed.blade.php
@@ -4,16 +4,16 @@
 
     <div class="row">
         <div class="col-md-6 col-md-offset-3">
-            <h2>Email Address not confirmed</h2>
-            <p class="text-muted">Your email address has not yet been confirmed. <br>
-                Please click the link in the email that was sent shortly after you registered. <br>
-                If you cannot find the email you can re-send the confirmation email by submitting the form below.
+            <h2>{{ trans('auth.email_not_confirmed') }}</h2>
+            <p class="text-muted">{{ trans('auth.email_not_confirmed_text') }}<br>
+                {{ trans('auth.email_not_confirmed_click_link') }} <br>
+                {{ trans('auth.email_not_confirmed_resend') }}
             </p>
             <hr>
             <form action="{{ baseUrl("/register/confirm/resend") }}" method="POST">
                 {!! csrf_field() !!}
                 <div class="form-group">
-                    <label for="email">Email Address</label>
+                    <label for="email">{{ trans('auth.email') }}</label>
                     @if(auth()->check())
                         @include('form/text', ['name' => 'email', 'model' => auth()->user()])
                     @else
@@ -21,7 +21,7 @@
                     @endif
                 </div>
                 <div class="form-group">
-                    <button type="submit" class="button pos">Resend Confirmation Email</button>
+                    <button type="submit" class="button pos">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
                 </div>
             </form>
         </div>
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index 08acf725d..43f22d89a 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -17,6 +17,7 @@
     <!-- Scripts -->
     <script src="{{ baseUrl('/libs/jquery/jquery.min.js?version=2.1.4') }}"></script>
     <script src="{{ baseUrl('/libs/jquery/jquery-ui.min.js?version=1.11.4') }}"></script>
+    <script src="{{ baseUrl('/translations.js') }}"></script>
 
     @yield('head')
 
@@ -53,32 +54,16 @@
                 <div class="col-lg-4 col-sm-5">
                     <div class="float right">
                         <div class="links text-center">
-                            <a href="{{ baseUrl('/books') }}"><i class="zmdi zmdi-book"></i>Books</a>
+                            <a href="{{ baseUrl('/books') }}"><i class="zmdi zmdi-book"></i>{{ trans('entities.books') }}</a>
                             @if(isset($currentUser) && userCan('settings-manage'))
-                                <a href="{{ baseUrl('/settings') }}"><i class="zmdi zmdi-settings"></i>Settings</a>
+                                <a href="{{ baseUrl('/settings') }}"><i class="zmdi zmdi-settings"></i>{{ trans('settings.settings') }}</a>
                             @endif
                             @if(!isset($signedIn) || !$signedIn)
-                                <a href="{{ baseUrl('/login') }}"><i class="zmdi zmdi-sign-in"></i>Sign In</a>
+                                <a href="{{ baseUrl('/login') }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
                             @endif
                         </div>
                         @if(isset($signedIn) && $signedIn)
-                            <div class="dropdown-container" dropdown>
-                                <span class="user-name" dropdown-toggle>
-                                    <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
-                                    <span class="name" ng-non-bindable>{{ $currentUser->getShortName(9) }}</span> <i class="zmdi zmdi-caret-down"></i>
-                                </span>
-                                <ul>
-                                    <li>
-                                        <a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>View Profile</a>
-                                    </li>
-                                    <li>
-                                        <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>Edit Profile</a>
-                                    </li>
-                                    <li>
-                                        <a href="{{ baseUrl('/logout') }}" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>Logout</a>
-                                    </li>
-                                </ul>
-                            </div>
+                            @include('partials._header-dropdown', ['currentUser' => $currentUser])
                         @endif
 
                     </div>
@@ -93,7 +78,7 @@
 
     <div id="back-to-top">
         <div class="inner">
-            <i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
+            <i class="zmdi zmdi-chevron-up"></i> <span>{{ trans('common.back_to_top') }}</span>
         </div>
     </div>
 @yield('bottom')
diff --git a/resources/views/books/_breadcrumbs.blade.php b/resources/views/books/_breadcrumbs.blade.php
new file mode 100644
index 000000000..e588127ce
--- /dev/null
+++ b/resources/views/books/_breadcrumbs.blade.php
@@ -0,0 +1,3 @@
+<div class="breadcrumbs">
+    <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
+</div>
\ No newline at end of file
diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php
index 60f4f65bd..2c629e699 100644
--- a/resources/views/books/create.blade.php
+++ b/resources/views/books/create.blade.php
@@ -3,7 +3,7 @@
 @section('content')
 
 <div class="container small" ng-non-bindable>
-    <h1>Create New Book</h1>
+    <h1>{{ trans('entities.books_create') }}</h1>
     <form action="{{ baseUrl("/books") }}" method="POST">
         @include('books/form')
     </form>
diff --git a/resources/views/books/delete.blade.php b/resources/views/books/delete.blade.php
index 68f755131..0b1e67d4a 100644
--- a/resources/views/books/delete.blade.php
+++ b/resources/views/books/delete.blade.php
@@ -2,16 +2,26 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    @include('books._breadcrumbs', ['book' => $book])
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
-        <h1>Delete Book</h1>
-        <p>This will delete the book with the name '{{$book->name}}', All pages and chapters will be removed.</p>
-        <p class="text-neg">Are you sure you want to delete this book?</p>
+        <h1>{{ trans('entities.books_delete') }}</h1>
+        <p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p>
+        <p class="text-neg">{{ trans('entities.books_delete_confirmation') }}</p>
 
         <form action="{{$book->getUrl()}}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="DELETE">
-            <a href="{{$book->getUrl()}}" class="button">Cancel</a>
-            <button type="submit" class="button neg">Confirm</button>
+            <a href="{{$book->getUrl()}}" class="button">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php
index e67e6f459..2419b68da 100644
--- a/resources/views/books/edit.blade.php
+++ b/resources/views/books/edit.blade.php
@@ -2,8 +2,18 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    @include('books._breadcrumbs', ['book' => $book])
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
-        <h1>Edit Book</h1>
+        <h1>{{ trans('entities.books_edit') }}</h1>
         <form action="{{ $book->getUrl() }}" method="POST">
             <input type="hidden" name="_method" value="PUT">
             @include('books/form', ['model' => $book])
diff --git a/resources/views/books/form.blade.php b/resources/views/books/form.blade.php
index dc0fd0a3f..514abf42c 100644
--- a/resources/views/books/form.blade.php
+++ b/resources/views/books/form.blade.php
@@ -1,16 +1,16 @@
 
 {{ csrf_field() }}
 <div class="form-group title-input">
-    <label for="name">Book Name</label>
+    <label for="name">{{ trans('common.name') }}</label>
     @include('form/text', ['name' => 'name'])
 </div>
 
 <div class="form-group description-input">
-    <label for="description">Description</label>
+    <label for="description">{{ trans('common.description') }}</label>
     @include('form/textarea', ['name' => 'description'])
 </div>
 
 <div class="form-group">
-    <a href="{{ back()->getTargetUrl() }}" class="button muted">Cancel</a>
-    <button type="submit" class="button pos">Save Book</button>
+    <a href="{{ back()->getTargetUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button type="submit" class="button pos">{{ trans('entities.books_save') }}</button>
 </div>
\ No newline at end of file
diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php
index 91906e7b8..c090a127e 100644
--- a/resources/views/books/index.blade.php
+++ b/resources/views/books/index.blade.php
@@ -9,7 +9,7 @@
                 <div class="col-xs-11 faded">
                     <div class="action-buttons">
                         @if($currentUser->can('book-create-all'))
-                            <a href="{{ baseUrl("/books/create") }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a>
+                            <a href="{{ baseUrl("/books/create") }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.books_create') }}</a>
                         @endif
                     </div>
                 </div>
@@ -21,7 +21,7 @@
     <div class="container" ng-non-bindable>
         <div class="row">
             <div class="col-sm-7">
-                <h1>Books</h1>
+                <h1>{{ trans('entities.books') }}</h1>
                 @if(count($books) > 0)
                     @foreach($books as $book)
                         @include('books/list-item', ['book' => $book])
@@ -29,27 +29,27 @@
                     @endforeach
                     {!! $books->render() !!}
                 @else
-                    <p class="text-muted">No books have been created.</p>
+                    <p class="text-muted">{{ trans('entities.books_empty') }}</p>
                     @if(userCan('books-create-all'))
-                        <a href="{{ baseUrl("/books/create") }}" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
+                        <a href="{{ baseUrl("/books/create") }}" class="text-pos"><i class="zmdi zmdi-edit"></i>{{ trans('entities.create_one_now') }}</a>
                     @endif
                 @endif
             </div>
             <div class="col-sm-4 col-sm-offset-1">
                 <div id="recents">
                     @if($recents)
-                        <div class="margin-top large">&nbsp;</div>
-                        <h3>Recently Viewed</h3>
+                        <div class="margin-top">&nbsp;</div>
+                        <h3>{{ trans('entities.recently_viewed') }}</h3>
                         @include('partials/entity-list', ['entities' => $recents])
                     @endif
                 </div>
                 <div class="margin-top large">&nbsp;</div>
                 <div id="popular">
-                    <h3>Popular Books</h3>
+                    <h3>{{ trans('entities.books_popular') }}</h3>
                     @if(count($popular) > 0)
                         @include('partials/entity-list', ['entities' => $popular])
                     @else
-                        <p class="text-muted">The most popular books will appear here.</p>
+                        <p class="text-muted">{{ trans('entities.books_popular_empty') }}</p>
                     @endif
                 </div>
             </div>
diff --git a/resources/views/books/restrictions.blade.php b/resources/views/books/restrictions.blade.php
index 7fdd3abef..f558fdfce 100644
--- a/resources/views/books/restrictions.blade.php
+++ b/resources/views/books/restrictions.blade.php
@@ -6,9 +6,7 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
-                    </div>
+                    @include('books._breadcrumbs', ['book' => $book])
                 </div>
             </div>
         </div>
@@ -16,7 +14,7 @@
 
 
     <div class="container" ng-non-bindable>
-        <h1>Book Permissions</h1>
+        <h1>{{ trans('entities.books_permissions') }}</h1>
         @include('form/restriction-form', ['model' => $book])
     </div>
 
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php
index 129851d5e..6a18302bc 100644
--- a/resources/views/books/show.blade.php
+++ b/resources/views/books/show.blade.php
@@ -5,29 +5,32 @@
     <div class="faded-small toolbar">
         <div class="container">
             <div class="row">
-                <div class="col-md-12">
+                <div class="col-md-6 faded">
+                    @include('books._breadcrumbs', ['book' => $book])
+                </div>
+                <div class="col-md-6">
                     <div class="action-buttons faded">
                         @if(userCan('page-create', $book))
-                            <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a>
+                            <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
                         @endif
                         @if(userCan('chapter-create', $book))
-                            <a href="{{ $book->getUrl('/chapter/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a>
+                            <a href="{{ $book->getUrl('/chapter/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.chapters_new') }}</a>
                         @endif
                         @if(userCan('book-update', $book))
-                            <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
+                            <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>{{ trans('common.edit') }}</a>
                         @endif
                         @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
                             <div dropdown class="dropdown-container">
                                 <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
                                 <ul>
                                     @if(userCan('book-update', $book))
-                                        <li><a href="{{ $book->getUrl('/sort') }}" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
+                                        <li><a href="{{ $book->getUrl('/sort') }}" class="text-primary"><i class="zmdi zmdi-sort"></i>{{ trans('common.sort') }}</a></li>
                                     @endif
                                     @if(userCan('restrictions-manage', $book))
-                                        <li><a href="{{ $book->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
+                                        <li><a href="{{ $book->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.permissions') }}</a></li>
                                     @endif
                                     @if(userCan('book-delete', $book))
-                                        <li><a href="{{ $book->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
+                                        <li><a href="{{ $book->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
                                     @endif
                                 </ul>
                             </div>
@@ -59,23 +62,19 @@
                                 <hr>
                             @endforeach
                         @else
-                            <p class="text-muted">No pages or chapters have been created for this book.</p>
+                            <p class="text-muted">{{ trans('entities.books_empty_contents') }}</p>
                             <p>
-                                <a href="{{ $book->getUrl('/page/create') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a>
-                                &nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp;
-                                <a href="{{ $book->getUrl('/chapter/create') }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>Add a chapter</a>
+                                <a href="{{ $book->getUrl('/page/create') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
+                                &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
+                                <a href="{{ $book->getUrl('/chapter/create') }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.books_empty_add_chapter') }}</a>
                             </p>
                             <hr>
                         @endif
-                        <p class="text-muted small">
-                            Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by <a href="{{ $book->createdBy->getProfileUrl() }}">{{$book->createdBy->name}}</a> @endif
-                            <br>
-                            Last Updated {{$book->updated_at->diffForHumans()}} @if($book->updatedBy) by <a href="{{ $book->updatedBy->getProfileUrl() }}">{{$book->updatedBy->name}}</a> @endif
-                        </p>
+                        @include('partials.entity-meta', ['entity' => $book])
                     </div>
                 </div>
                 <div class="search-results" ng-cloak ng-show="searching">
-                    <h3 class="text-muted">Search Results <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>Clear Search</a></h3>
+                    <h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
                     <div ng-if="!searchResults">
                         @include('partials/loading-icon')
                     </div>
@@ -90,21 +89,21 @@
                 @if($book->restricted)
                     <p class="text-muted">
                         @if(userCan('restrictions-manage', $book))
-                            <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
+                            <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}</a>
                         @else
-                            <i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
+                            <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}
                         @endif
                     </p>
                 @endif
                 <div class="search-box">
                     <form ng-submit="searchBook($event)">
-                        <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">
+                        <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
                         <button type="submit"><i class="zmdi zmdi-search"></i></button>
                         <button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
                     </form>
                 </div>
                 <div class="activity anim fadeIn">
-                    <h3>Recent Activity</h3>
+                    <h3>{{ trans('entities.recent_activity') }}</h3>
                     @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
                 </div>
             </div>
diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php
index 984db0ce6..d96f502f1 100644
--- a/resources/views/books/sort.blade.php
+++ b/resources/views/books/sort.blade.php
@@ -6,8 +6,18 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    @include('books._breadcrumbs', ['book' => $book])
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container" ng-non-bindable>
-        <h1>Sorting Pages & Chapters<span class="subheader">For {{ $book->name }}</span></h1>
+        <h1>{{ trans('entities.books_sort') }}</h1>
         <div class="row">
             <div class="col-md-8" id="sort-boxes">
 
@@ -17,7 +27,7 @@
 
             @if(count($books) > 1)
                 <div class="col-md-4">
-                    <h3>Show Other Books</h3>
+                    <h3>{{ trans('entities.books_sort_show_other') }}</h3>
                     <div id="additional-books">
                     @foreach($books as $otherBook)
                         @if($otherBook->id !== $book->id)
@@ -37,8 +47,8 @@
             <input type="hidden" name="_method" value="PUT">
             <input type="hidden" id="sort-tree-input" name="sort-tree">
             <div class="list">
-                <a href="{{ $book->getUrl() }}" class="button muted">Cancel</a>
-                <button class="button pos" type="submit">Save Order</button>
+                <a href="{{ $book->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+                <button class="button pos" type="submit">{{ trans('entities.books_sort_save') }}</button>
             </div>
         </form>
 
diff --git a/resources/views/chapters/_breadcrumbs.blade.php b/resources/views/chapters/_breadcrumbs.blade.php
new file mode 100644
index 000000000..9064cc7c3
--- /dev/null
+++ b/resources/views/chapters/_breadcrumbs.blade.php
@@ -0,0 +1,5 @@
+<div class="breadcrumbs">
+    <a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
+    <span class="sep">&raquo;</span>
+    <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
+</div>
\ No newline at end of file
diff --git a/resources/views/chapters/create.blade.php b/resources/views/chapters/create.blade.php
index b81cb15d7..afdbfa99d 100644
--- a/resources/views/chapters/create.blade.php
+++ b/resources/views/chapters/create.blade.php
@@ -3,7 +3,7 @@
 @section('content')
 
     <div class="container small" ng-non-bindable>
-        <h1>Create New Chapter</h1>
+        <h1>{{ trans('entities.chapters_create') }}</h1>
         <form action="{{ $book->getUrl('/chapter/create') }}" method="POST">
             @include('chapters/form')
         </form>
diff --git a/resources/views/chapters/delete.blade.php b/resources/views/chapters/delete.blade.php
index e9573f228..bacb8dca3 100644
--- a/resources/views/chapters/delete.blade.php
+++ b/resources/views/chapters/delete.blade.php
@@ -2,17 +2,26 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    @include('chapters._breadcrumbs', ['chapter' => $chapter])
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
-        <h1>Delete Chapter</h1>
-        <p>This will delete the chapter with the name '{{$chapter->name}}', All pages will be removed
-        and added directly to the book.</p>
-        <p class="text-neg">Are you sure you want to delete this chapter?</p>
+        <h1>{{ trans('entities.chapters_delete') }}</h1>
+        <p>{{ trans('entities.chapters_delete_explain', ['chapterName' => $chapter->name]) }}</p>
+        <p class="text-neg">{{ trans('entities.chapters_delete_confirm') }}</p>
 
         <form action="{{ $chapter->getUrl() }}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="DELETE">
-            <a href="{{ $chapter->getUrl() }}" class="button primary">Cancel</a>
-            <button type="submit" class="button neg">Confirm</button>
+            <a href="{{ $chapter->getUrl() }}" class="button primary">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/chapters/edit.blade.php b/resources/views/chapters/edit.blade.php
index 0363da96d..272543e67 100644
--- a/resources/views/chapters/edit.blade.php
+++ b/resources/views/chapters/edit.blade.php
@@ -3,7 +3,7 @@
 @section('content')
 
     <div class="container small" ng-non-bindable>
-        <h1>Edit Chapter</h1>
+        <h1>{{ trans('entities.chapters_edit') }}</h1>
         <form action="{{  $chapter->getUrl() }}" method="POST">
             <input type="hidden" name="_method" value="PUT">
             @include('chapters/form', ['model' => $chapter])
diff --git a/resources/views/chapters/form.blade.php b/resources/views/chapters/form.blade.php
index 70df4737a..54722a58a 100644
--- a/resources/views/chapters/form.blade.php
+++ b/resources/views/chapters/form.blade.php
@@ -2,16 +2,16 @@
 {!! csrf_field() !!}
 
 <div class="form-group title-input">
-    <label for="name">Chapter Name</label>
+    <label for="name">{{ trans('common.name') }}</label>
     @include('form/text', ['name' => 'name'])
 </div>
 
 <div class="form-group description-input">
-    <label for="description">Description</label>
+    <label for="description">{{ trans('common.description') }}</label>
     @include('form/textarea', ['name' => 'description'])
 </div>
 
 <div class="form-group">
-    <a href="{{ back()->getTargetUrl() }}" class="button muted">Cancel</a>
-    <button type="submit" class="button pos">Save Chapter</button>
+    <a href="{{ back()->getTargetUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button type="submit" class="button pos">{{ trans('entities.chapters_save') }}</button>
 </div>
diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php
index f70e59244..8487a63a3 100644
--- a/resources/views/chapters/list-item.blade.php
+++ b/resources/views/chapters/list-item.blade.php
@@ -17,7 +17,7 @@
     @endif
 
     @if(!isset($hidePages) && count($chapter->pages) > 0)
-        <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
+        <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p>
         <div class="inset-list">
             @foreach($chapter->pages as $page)
                 <h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>
diff --git a/resources/views/chapters/move.blade.php b/resources/views/chapters/move.blade.php
index 37d56d30d..9e6ddb521 100644
--- a/resources/views/chapters/move.blade.php
+++ b/resources/views/chapters/move.blade.php
@@ -6,27 +6,23 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
-                        <span class="sep">&raquo;</span>
-                        <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->getShortName() }}</a>
-                    </div>
+                    @include('chapters._breadcrumbs', ['chapter' => $chapter])
                 </div>
             </div>
         </div>
     </div>
 
     <div class="container">
-        <h1>Move Chapter <small class="subheader">{{$chapter->name}}</small></h1>
+        <h1>{{ trans('entities.chapters_move') }}</h1>
 
         <form action="{{ $chapter->getUrl('/move') }}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="PUT">
 
-            @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
+            @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
 
-            <a href="{{ $chapter->getUrl() }}" class="button muted">Cancel</a>
-            <button type="submit" class="button pos">Move Chapter</button>
+            <a href="{{ $chapter->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button pos">{{ trans('entities.chapters_move') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/chapters/restrictions.blade.php b/resources/views/chapters/restrictions.blade.php
index 771665037..7b908ee15 100644
--- a/resources/views/chapters/restrictions.blade.php
+++ b/resources/views/chapters/restrictions.blade.php
@@ -6,18 +6,14 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
-                        <span class="sep">&raquo;</span>
-                        <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
-                    </div>
+                    @include('chapters._breadcrumbs', ['chapter' => $chapter])
                 </div>
             </div>
         </div>
     </div>
 
     <div class="container" ng-non-bindable>
-        <h1>Chapter Permissions</h1>
+        <h1>{{ trans('entities.chapters_permissions') }}</h1>
         @include('form/restriction-form', ['model' => $chapter])
     </div>
 
diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php
index 70b09e9ce..93eee6424 100644
--- a/resources/views/chapters/show.blade.php
+++ b/resources/views/chapters/show.blade.php
@@ -6,30 +6,28 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-8 faded" ng-non-bindable>
-                    <div class="breadcrumbs">
-                        <a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
-                    </div>
+                    @include('chapters._breadcrumbs', ['chapter' => $chapter])
                 </div>
                 <div class="col-sm-4 faded">
                     <div class="action-buttons">
                         @if(userCan('page-create', $chapter))
-                            <a href="{{ $chapter->getUrl('/create-page') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a>
+                            <a href="{{ $chapter->getUrl('/create-page') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
                         @endif
                         @if(userCan('chapter-update', $chapter))
-                            <a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
+                            <a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>{{ trans('common.edit') }}</a>
                         @endif
                         @if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
                             <div dropdown class="dropdown-container">
                                 <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
                                 <ul>
                                     @if(userCan('chapter-update', $chapter))
-                                        <li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary"><i class="zmdi zmdi-folder"></i>Move</a></li>
+                                        <li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary"><i class="zmdi zmdi-folder"></i>{{ trans('common.move') }}</a></li>
                                     @endif
                                     @if(userCan('restrictions-manage', $chapter))
-                                        <li><a href="{{ $chapter->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
+                                        <li><a href="{{ $chapter->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.permissions') }}</a></li>
                                     @endif
                                     @if(userCan('chapter-delete', $chapter))
-                                        <li><a href="{{ $chapter->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
+                                        <li><a href="{{ $chapter->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
                                     @endif
                                 </ul>
                             </div>
@@ -57,26 +55,22 @@
                     </div>
                 @else
                     <hr>
-                    <p class="text-muted">No pages are currently in this chapter.</p>
+                    <p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
                     <p>
                         @if(userCan('page-create', $chapter))
-                            <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a>
+                            <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
                         @endif
                         @if(userCan('page-create', $chapter) && userCan('book-update', $book))
-                            &nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp;
+                            &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
                         @endif
                         @if(userCan('book-update', $book))
-                            <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>Sort the current book</a>
+                            <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
                         @endif
                     </p>
                     <hr>
                 @endif
 
-                <p class="text-muted small">
-                    Created {{ $chapter->created_at->diffForHumans() }} @if($chapter->createdBy) by <a href="{{ $chapter->createdBy->getProfileUrl() }}">{{ $chapter->createdBy->name}}</a> @endif
-                    <br>
-                    Last Updated {{ $chapter->updated_at->diffForHumans() }} @if($chapter->updatedBy) by <a href="{{ $chapter->updatedBy->getProfileUrl() }}">{{  $chapter->updatedBy->name}}</a> @endif
-                </p>
+                @include('partials.entity-meta', ['entity' => $chapter])
             </div>
             <div class="col-md-3 col-md-offset-1">
                 <div class="margin-top large"></div>
@@ -84,19 +78,20 @@
                     <div class="text-muted">
 
                         @if($book->restricted)
-                            @if(userCan('restrictions-manage', $book))
-                                <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
-                            @else
-                                <i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
-                            @endif
-                                <br>
+                            <p class="text-muted">
+                                @if(userCan('restrictions-manage', $book))
+                                    <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}</a>
+                                @else
+                                    <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}
+                                @endif
+                            </p>
                         @endif
 
                         @if($chapter->restricted)
                             @if(userCan('restrictions-manage', $chapter))
-                                <a href="{{ $chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active</a>
+                                <a href="{{ $chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}</a>
                             @else
-                                <i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active
+                                <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}
                             @endif
                         @endif
                     </div>
diff --git a/resources/views/partials/entity-selector-popup.blade.php b/resources/views/components/entity-selector-popup.blade.php
similarity index 63%
rename from resources/views/partials/entity-selector-popup.blade.php
rename to resources/views/components/entity-selector-popup.blade.php
index b9166896a..1c4d1fadb 100644
--- a/resources/views/partials/entity-selector-popup.blade.php
+++ b/resources/views/components/entity-selector-popup.blade.php
@@ -2,12 +2,12 @@
     <div class="overlay" entity-link-selector>
         <div class="popup-body small flex-child">
             <div class="popup-header primary-background">
-                <div class="popup-title">Entity Select</div>
+                <div class="popup-title">{{ trans('entities.entity_select') }}</div>
                 <button type="button" class="corner-button neg button popup-close">x</button>
             </div>
-            @include('partials/entity-selector', ['name' => 'entity-selector'])
+            @include('components.entity-selector', ['name' => 'entity-selector'])
             <div class="popup-footer">
-                <button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">Select</button>
+                <button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">{{ trans('common.select') }}</button>
             </div>
         </div>
     </div>
diff --git a/resources/views/partials/entity-selector.blade.php b/resources/views/components/entity-selector.blade.php
similarity index 67%
rename from resources/views/partials/entity-selector.blade.php
rename to resources/views/components/entity-selector.blade.php
index 59e174155..8fb2187e6 100644
--- a/resources/views/partials/entity-selector.blade.php
+++ b/resources/views/components/entity-selector.blade.php
@@ -1,8 +1,8 @@
 <div class="form-group">
     <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
         <input type="hidden" entity-selector-input name="{{$name}}" value="">
-        <input type="text" placeholder="Search" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
-        <div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div>
+        <input type="text" placeholder="{{ trans('common.search') }}" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
+        <div class="text-center loading" ng-show="loading">@include('partials.loading-icon')</div>
         <div ng-show="!loading" ng-bind-html="entityResults"></div>
     </div>
 </div>
\ No newline at end of file
diff --git a/resources/views/partials/image-manager.blade.php b/resources/views/components/image-manager.blade.php
similarity index 70%
rename from resources/views/partials/image-manager.blade.php
rename to resources/views/components/image-manager.blade.php
index 83625ad88..39f3bcd3c 100644
--- a/resources/views/partials/image-manager.blade.php
+++ b/resources/views/components/image-manager.blade.php
@@ -3,7 +3,7 @@
         <div class="popup-body" ng-click="$event.stopPropagation()">
 
             <div class="popup-header primary-background">
-                <div class="popup-title">Image Select</div>
+                <div class="popup-title">{{ trans('components.image_select') }}</div>
                 <button class="popup-close neg corner-button button">x</button>
             </div>
 
@@ -12,16 +12,16 @@
                 <div class="image-manager-content">
                     <div ng-if="imageType === 'gallery'" class="container">
                         <div class="image-manager-header row faded-small nav-tabs">
-                            <div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
-                            <div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
-                            <div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
+                            <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div>
+                            <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
+                            <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
                         </div>
                     </div>
                     <div ng-show="view === 'all'" >
                         <form ng-submit="searchImages()" class="contained-search-box">
-                            <input type="text" placeholder="Search by image name" ng-model="searchTerm">
-                            <button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
-                            <button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
+                            <input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm">
+                            <button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
+                            <button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
                         </form>
                     </div>
                     <div class="image-manager-list">
@@ -31,11 +31,11 @@
                                 <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
                                 <div class="image-meta">
                                     <span class="name" ng-bind="image.name"></span>
-                                    <span class="date">Uploaded @{{ getDate(image.created_at)  }}</span>
+                                    <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
                                 </div>
                             </div>
                         </div>
-                        <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
+                        <div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div>
                     </div>
                 </div>
 
@@ -51,15 +51,14 @@
                                     </a>
                                 </div>
                                 <div class="form-group">
-                                    <label for="name">Image Name</label>
+                                    <label for="name">{{ trans('components.image_image_name') }}</label>
                                     <input type="text" id="name" name="name" ng-model="selectedImage.name">
                                 </div>
                             </form>
 
                             <div ng-show="dependantPages">
                                 <p class="text-neg text-small">
-                                    This image is used in the pages below, Click delete again to confirm you want to delete
-                                    this image.
+                                    {{ trans('components.image_delete_confirm') }}
                                 </p>
                                 <ul class="text-neg">
                                     <li ng-repeat="page in dependantPages">
@@ -73,13 +72,13 @@
                                     <button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
                                 </form>
                                 <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
-                                    <i class="zmdi zmdi-square-right"></i>Select Image
+                                    <i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }}
                                 </button>
                             </div>
 
                         </div>
 
-                        <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
+                        <drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
 
 
                     </div>
diff --git a/resources/views/components/image-picker.blade.php b/resources/views/components/image-picker.blade.php
new file mode 100644
index 000000000..47fb2b8b7
--- /dev/null
+++ b/resources/views/components/image-picker.blade.php
@@ -0,0 +1,66 @@
+<div class="image-picker" image-picker="{{$name}}" data-default-image="{{ $defaultImage }}" data-resize-height="{{ $resizeHeight }}" data-resize-width="{{ $resizeWidth }}" data-current-id="{{ $currentId or '' }}" data-resize-crop="{{ $resizeCrop or '' }}">
+
+    <div>
+        <img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif  class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
+    </div>
+
+    <button class="button" type="button" data-action="show-image-manager">{{ trans('components.image_select_image') }}</button>
+    <br>
+    <button class="text-button" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
+
+    @if ($showRemove)
+        <span class="sep">|</span>
+        <button class="text-button neg" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
+    @endif
+
+    <input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== '' && $currentId !== false) ? $currentId : $currentImage}}">
+</div>
+
+<script>
+    (function(){
+
+        var picker = document.querySelector('[image-picker="{{$name}}"]');
+        picker.addEventListener('click', function(event) {
+            if (event.target.nodeName.toLowerCase() !== 'button') return;
+             var button = event.target;
+             var action = button.getAttribute('data-action');
+             var resize = picker.getAttribute('data-resize-height') && picker.getAttribute('data-resize-width');
+             var usingIds = picker.getAttribute('data-current-id') !== '';
+             var resizeCrop = picker.getAttribute('data-resize-crop') !== '';
+             var imageElem = picker.querySelector('img');
+             var input = picker.querySelector('input');
+
+             function setImage(image) {
+                 if (image === 'none') {
+                     imageElem.src = picker.getAttribute('data-default-image');
+                     imageElem.classList.add('none');
+                     input.value = 'none';
+                     return;
+                 }
+                 imageElem.src = image.url;
+                 input.value = usingIds ? image.id : image.url;
+                 imageElem.classList.remove('none');
+             }
+
+             if (action === 'show-image-manager') {
+                 window.ImageManager.showExternal((image) => {
+                     if (!resize) {
+                         setImage(image);
+                         return;
+                     }
+                     var requestString = '/images/thumb/' + image.id + '/' + picker.getAttribute('data-resize-width') + '/' + picker.getAttribute('data-resize-height') + '/' + (resizeCrop ? 'true' : 'false');
+                     $.get(window.baseUrl(requestString), resp => {
+                         image.url = resp.url;
+                         setImage(image);
+                     });
+                 });
+             } else if (action === 'reset-image') {
+                 setImage({id: 0, url: picker.getAttribute('data-default-image')});
+             } else if (action === 'remove-image') {
+                 setImage('none');
+             }
+
+            });
+
+    })();
+</script>
\ No newline at end of file
diff --git a/resources/views/components/toggle-switch.blade.php b/resources/views/components/toggle-switch.blade.php
new file mode 100644
index 000000000..ad54d5ab1
--- /dev/null
+++ b/resources/views/components/toggle-switch.blade.php
@@ -0,0 +1,15 @@
+<div toggle-switch="{{$name}}" class="toggle-switch @if($value) active @endif">
+    <input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
+    <div class="switch-handle"></div>
+</div>
+<script>
+    (function() {
+       var toggle = document.querySelector('[toggle-switch="{{$name}}"]');
+       var toggleInput = toggle.querySelector('input');
+       toggle.onclick = function(event) {
+           var checked = toggleInput.value !== 'true';
+           toggleInput.value = checked ? 'true' : 'false';
+           checked ? toggle.classList.add('active') : toggle.classList.remove('active');
+       };
+    })()
+</script>
\ No newline at end of file
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php
index 19565bccb..c9e600ceb 100644
--- a/resources/views/errors/404.blade.php
+++ b/resources/views/errors/404.blade.php
@@ -4,9 +4,28 @@
 
 
 <div class="container">
-    <h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
-    <p>Sorry, The page you were looking for could not be found.</p>
-    <a href="{{ baseUrl('/') }}" class="button">Return To Home</a>
+
+
+    <h1>{{ $message or trans('errors.404_page_not_found') }}</h1>
+    <p>{{ trans('errors.sorry_page_not_found') }}</p>
+    <p><a href="{{ baseUrl('/') }}" class="button">{{ trans('errors.return_home') }}</a></p>
+
+    <hr>
+
+    <div class="row">
+        <div class="col-md-4">
+            <h3 class="text-muted">{{ trans('entities.pages_popular') }}</h3>
+            @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Page::class]), 'style' => 'compact'])
+        </div>
+        <div class="col-md-4">
+            <h3 class="text-muted">{{ trans('entities.books_popular') }}</h3>
+            @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Book::class]), 'style' => 'compact'])
+        </div>
+        <div class="col-md-4">
+            <h3 class="text-muted">{{ trans('entities.chapters_popular') }}</h3>
+            @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Chapter::class]), 'style' => 'compact'])
+        </div>
+    </div>
 </div>
 
 @stop
\ No newline at end of file
diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php
index 2a58461ba..6dd96cdcc 100644
--- a/resources/views/errors/500.blade.php
+++ b/resources/views/errors/500.blade.php
@@ -3,7 +3,7 @@
 @section('content')
 
     <div class="container">
-        <h1 class="text-muted">An Error Occurred</h1>
+        <h1 class="text-muted">{{ trans('errors.error_occurred') }}</h1>
         <p>{{ $message }}</p>
     </div>
 
diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php
index c79d0f68b..1ea39a7b8 100644
--- a/resources/views/errors/503.blade.php
+++ b/resources/views/errors/503.blade.php
@@ -3,8 +3,8 @@
 @section('content')
 
     <div class="container">
-        <h1 class="text-muted">{{ setting('app-name') }} is down right now</h1>
-        <p>It will be back up soon.</p>
+        <h1 class="text-muted">{{ trans('errors.app_down', ['appName' => setting('app-name')]) }}</h1>
+        <p>{{ trans('errors.back_soon') }}</p>
     </div>
 
 @stop
\ No newline at end of file
diff --git a/resources/views/form/delete-button.blade.php b/resources/views/form/delete-button.blade.php
index a5b1f2809..6c53effae 100644
--- a/resources/views/form/delete-button.blade.php
+++ b/resources/views/form/delete-button.blade.php
@@ -1,5 +1,5 @@
 <form action="{{$url}}" method="POST" class="inline">
     {{ csrf_field() }}
     <input type="hidden" name="_method" value="DELETE">
-    <button type="submit" class="button neg">{{ isset($text) ? $text : 'Delete' }}</button>
+    <button type="submit" class="button neg">{{ isset($text) ? $text : trans('common.delete') }}</button>
 </form>
\ No newline at end of file
diff --git a/resources/views/form/restriction-form.blade.php b/resources/views/form/restriction-form.blade.php
index 7472fe65e..7a1605197 100644
--- a/resources/views/form/restriction-form.blade.php
+++ b/resources/views/form/restriction-form.blade.php
@@ -2,31 +2,31 @@
     {!! csrf_field() !!}
     <input type="hidden" name="_method" value="PUT">
 
-    <p>Once enabled, These permissions will take priority over any set role permissions.</p>
+    <p>{{ trans('entities.permissions_intro') }}</p>
 
     <div class="form-group">
-        @include('form/checkbox', ['name' => 'restricted', 'label' => 'Enable custom permissions'])
+        @include('form/checkbox', ['name' => 'restricted', 'label' => trans('entities.permissions_enable')])
     </div>
 
 
     <table class="table">
         <tr>
-            <th>Role</th>
-            <th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>Actions</th>
+            <th>{{ trans('common.role') }}</th>
+            <th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>{{ trans('common.actions') }}</th>
         </tr>
         @foreach($roles as $role)
             <tr>
                 <td>{{ $role->display_name }}</td>
-                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'View', 'action' => 'view'])</td>
+                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.view'), 'action' => 'view'])</td>
                 @if(!$model->isA('page'))
-                    <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Create', 'action' => 'create'])</td>
+                    <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.create'), 'action' => 'create'])</td>
                 @endif
-                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Update', 'action' => 'update'])</td>
-                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Delete', 'action' => 'delete'])</td>
+                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.update'), 'action' => 'update'])</td>
+                <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.delete'), 'action' => 'delete'])</td>
             </tr>
         @endforeach
     </table>
 
-    <a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
-    <button type="submit" class="button pos">Save Permissions</button>
+    <a href="{{ $model->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button type="submit" class="button pos">{{ trans('entities.permissions_save') }}</button>
 </form>
\ No newline at end of file
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
index 2fb4ac855..0d97a6a4b 100644
--- a/resources/views/home.blade.php
+++ b/resources/views/home.blade.php
@@ -5,14 +5,9 @@
     <div class="faded-small toolbar">
         <div class="container">
             <div class="row">
-                <div class="col-sm-4 faded">
+                <div class="col-sm-6 faded">
                     <div class="action-buttons text-left">
-                        <a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>Toggle Details</a>
-                    </div>
-                </div>
-                <div class="col-sm-8 faded">
-                    <div class="action-buttons">
-
+                        <a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>{{ trans('common.toggle_details') }}</a>
                     </div>
                 </div>
             </div>
@@ -25,44 +20,44 @@
             <div class="col-sm-4">
                 <div id="recent-drafts">
                     @if(count($draftPages) > 0)
-                        <h4>My Recent Drafts</h4>
+                        <h4>{{ trans('entities.my_recent_drafts') }}</h4>
                         @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
                     @endif
                 </div>
                 @if($signedIn)
-                    <h4>My Recently Viewed</h4>
+                    <h4>{{ trans('entities.my_recently_viewed') }}</h4>
                 @else
-                    <h4>Recent Books</h4>
+                    <h4>{{ trans('entities.books_recent') }}</h4>
                 @endif
                 @include('partials/entity-list', [
                 'entities' => $recents,
                 'style' => 'compact',
-                'emptyText' => $signedIn ? 'You have not viewed any pages' : 'No books have been created'
+                'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
                 ])
             </div>
 
             <div class="col-sm-4">
-                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h4>
+                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">{{ trans('entities.recently_created_pages') }}</a></h4>
                 <div id="recently-created-pages">
                     @include('partials/entity-list', [
                     'entities' => $recentlyCreatedPages,
                     'style' => 'compact',
-                    'emptyText' => 'No pages have been recently created'
+                    'emptyText' => trans('entities.no_pages_recently_created')
                     ])
                 </div>
 
-                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h4>
+                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h4>
                 <div id="recently-updated-pages">
                     @include('partials/entity-list', [
                     'entities' => $recentlyUpdatedPages,
                     'style' => 'compact',
-                    'emptyText' => 'No pages have been recently updated'
+                    'emptyText' => trans('entites.no_pages_recently_updated')
                     ])
                 </div>
             </div>
 
             <div class="col-sm-4" id="recent-activity">
-                <h4>Recent Activity</h4>
+                <h4>{{ trans('entities.recent_activity') }}</h4>
                 @include('partials/activity-list', ['activity' => $activity])
             </div>
 
diff --git a/resources/views/pages/_breadcrumbs.blade.php b/resources/views/pages/_breadcrumbs.blade.php
new file mode 100644
index 000000000..0d2a61ab2
--- /dev/null
+++ b/resources/views/pages/_breadcrumbs.blade.php
@@ -0,0 +1,12 @@
+<div class="breadcrumbs">
+    <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
+    @if($page->hasChapter())
+        <span class="sep">&raquo;</span>
+        <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
+            <i class="zmdi zmdi-collection-bookmark"></i>
+            {{ $page->chapter->getShortName() }}
+        </a>
+    @endif
+    <span class="sep">&raquo;</span>
+    <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
+</div>
\ No newline at end of file
diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php
index 57cc86054..f94a614fb 100644
--- a/resources/views/pages/delete.blade.php
+++ b/resources/views/pages/delete.blade.php
@@ -2,15 +2,25 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    @include('pages._breadcrumbs', ['page' => $page])
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
-        <h1>Delete {{ $page->draft ? 'Draft' : '' }} Page</h1>
-        <p class="text-neg">Are you sure you want to delete this {{ $page->draft ? 'draft' : '' }} page?</p>
+        <h1>{{ $page->draft ? trans('entities.pages_delete_draft') : trans('entities.pages_delete') }}</h1>
+        <p class="text-neg">{{ $page->draft ? trans('entities.pages_delete_draft_confirm'): trans('entities.pages_delete_confirm') }}</p>
 
         <form action="{{ $page->getUrl() }}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="DELETE">
-            <a href="{{ $page->getUrl() }}" class="button primary">Cancel</a>
-            <button type="submit" class="button neg">Confirm</button>
+            <a href="{{ $page->getUrl() }}" class="button primary">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php
index e50cc7c5b..e1c0a169d 100644
--- a/resources/views/pages/edit.blade.php
+++ b/resources/views/pages/edit.blade.php
@@ -20,7 +20,7 @@
 
     </div>
     
-    @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
-    @include('partials/entity-selector-popup')
+    @include('components.image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
+    @include('components.entity-selector-popup')
 
 @stop
\ No newline at end of file
diff --git a/resources/views/pages/export.blade.php b/resources/views/pages/export.blade.php
index 96f06290e..19a635563 100644
--- a/resources/views/pages/export.blade.php
+++ b/resources/views/pages/export.blade.php
@@ -15,15 +15,11 @@
         <div class="col-md-8 col-md-offset-2">
             <div class="page-content">
 
-                @include('pages/page-display')
+                @include('pages.page-display')
 
                 <hr>
 
-                <p class="text-muted small">
-                    Created {{$page->created_at->toDayDateTimeString()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
-                    <br>
-                    Last Updated {{$page->updated_at->toDayDateTimeString()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
-                </p>
+                @include('partials.entity-meta', ['entity' => $page])
 
             </div>
         </div>
diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php
index a6e66a24a..ecf7619b7 100644
--- a/resources/views/pages/form-toolbox.blade.php
+++ b/resources/views/pages/form-toolbox.blade.php
@@ -3,22 +3,22 @@
 
     <div class="tabs primary-background-light">
         <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
-        <span toolbox-tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
+        <span toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active"><i class="zmdi zmdi-tag"></i></span>
         @if(userCan('attachment-create-all'))
-            <span toolbox-tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
+            <span toolbox-tab-button="files" title="{{ trans('entities.attachments') }}"><i class="zmdi zmdi-attachment"></i></span>
         @endif
     </div>
 
     <div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
-        <h4>Page Tags</h4>
+        <h4>{{ trans('entities.page_tags') }}</h4>
         <div class="padded tags">
-            <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
+            <p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
             <table class="no-style" tag-autosuggestions style="width: 100%;">
                 <tbody ui-sortable="sortOptions" ng-model="tags" >
                     <tr ng-repeat="tag in tags track by $index">
                         <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
-                        <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/names') }}" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
-                        <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/values') }}" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
+                        <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/names') }}" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"></td>
+                        <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/values') }}" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"></td>
                         <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td>
                     </tr>
                 </tbody>
@@ -28,7 +28,7 @@
                 <tr class="unsortable">
                     <td  width="34"></td>
                     <td ng-click="addEmptyTag()">
-                        <button type="button" class="text-button">Add another tag</button>
+                        <button type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
                     </td>
                     <td></td>
                 </tr>
@@ -39,17 +39,17 @@
 
     @if(userCan('attachment-create-all'))
         <div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
-            <h4>Attachments</h4>
+            <h4>{{ trans('entities.attachments') }}</h4>
             <div class="padded files">
 
                 <div id="file-list" ng-show="!editFile">
-                    <p class="muted small">Upload some files or attach some link to display on your page. These are visible in the page sidebar. <span class="secondary">Changes here are saved instantly.</span></p>
+                    <p class="muted small">{{ trans('entities.attachments_explain') }} <span class="secondary">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
 
                     <div tab-container>
                         <div class="nav-tabs">
-                            <div tab-button="list" class="tab-item">Attached Items</div>
-                            <div tab-button="file" class="tab-item">Upload File</div>
-                            <div tab-button="link" class="tab-item">Attach Link</div>
+                            <div tab-button="list" class="tab-item">{{ trans('entities.attachments_items') }}</div>
+                            <div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
+                            <div tab-button="link" class="tab-item">{{ trans('entities.attachments_link') }}</div>
                         </div>
                         <div tab-content="list">
                             <table class="file-table" style="width: 100%;">
@@ -59,9 +59,9 @@
                                     <td>
                                         <a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
                                         <div ng-if="file.deleting">
-                                            <span class="neg small">Click delete again to confirm you want to delete this attachment.</span>
+                                            <span class="neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
                                             <br>
-                                            <span class="text-primary small" ng-click="file.deleting=false;">Cancel</span>
+                                            <span class="text-primary small" ng-click="file.deleting=false;">{{ trans('common.cancel') }}</span>
                                         </div>
                                     </td>
                                     <td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
@@ -71,25 +71,25 @@
                                 </tbody>
                             </table>
                             <p class="small muted" ng-if="files.length == 0">
-                                No files have been uploaded.
+                                {{ trans('entities.attachments_no_files') }}
                             </p>
                         </div>
                         <div tab-content="file">
-                            <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
+                            <drop-zone message="{{ trans('entities.attachments_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
                         </div>
                         <div tab-content="link" sub-form="attachLinkSubmit(file)">
-                            <p class="muted small">You can attach a link if you'd prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.</p>
+                            <p class="muted small">{{ trans('entities.attachments_explain_link') }}</p>
                             <div class="form-group">
-                                <label for="attachment-via-link">Link Name</label>
-                                <input type="text" placeholder="Link name" ng-model="file.name">
+                                <label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
+                                <input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" ng-model="file.name">
                                 <p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
                             </div>
                             <div class="form-group">
-                                <label for="attachment-via-link">Link to file</label>
-                                <input type="text" placeholder="Url of site or file" ng-model="file.link">
+                                <label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
+                                <input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" ng-model="file.link">
                                 <p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
                             </div>
-                            <button type="submit" class="button pos">Attach</button>
+                            <button type="submit" class="button pos">{{ trans('entities.attach') }}</button>
 
                         </div>
                     </div>
@@ -97,34 +97,34 @@
                 </div>
 
                 <div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
-                    <h5>Edit File</h5>
+                    <h5>{{ trans('entities.attachments_edit_file') }}</h5>
 
                     <div class="form-group">
-                        <label for="attachment-name-edit">File Name</label>
-                        <input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
+                        <label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label>
+                        <input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" ng-model="editFile.name">
                         <p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
                     </div>
 
                     <div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
                         <div class="nav-tabs">
-                            <div tab-button="file" class="tab-item">Upload File</div>
-                            <div tab-button="link" class="tab-item">Set Link</div>
+                            <div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
+                            <div tab-button="link" class="tab-item">{{ trans('entities.attachments_set_link') }}</div>
                         </div>
                         <div tab-content="file">
-                            <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
+                            <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" event-success="uploadSuccessUpdate"></drop-zone>
                             <br>
                         </div>
                         <div tab-content="link">
                             <div class="form-group">
-                                <label for="attachment-link-edit">Link to file</label>
-                                <input type="text" id="attachment-link-edit" placeholder="Attachment link" ng-model="editFile.link">
+                                <label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
+                                <input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" ng-model="editFile.link">
                                 <p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
                             </div>
                         </div>
                     </div>
 
-                    <button type="button" class="button" ng-click="cancelEdit()">Back</button>
-                    <button type="submit" class="button pos">Save</button>
+                    <button type="button" class="button" ng-click="cancelEdit()">{{ trans('common.back') }}</button>
+                    <button type="submit" class="button pos">{{ trans('common.save') }}</button>
                 </div>
 
             </div>
diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php
index c4baf38f7..eb5ebb0bd 100644
--- a/resources/views/pages/form.blade.php
+++ b/resources/views/pages/form.blade.php
@@ -9,8 +9,8 @@
             <div class="row">
                 <div class="col-sm-4 faded">
                     <div class="action-buttons text-left">
-                        <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-arrow-left"></i>Back</a>
-                        <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
+                        <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-arrow-left"></i>{{ trans('common.back') }}</a>
+                        <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>{{ trans('entities.pages_edit_toggle_header') }}</a>
                     </div>
                 </div>
                 <div class="col-sm-4 faded text-center">
@@ -20,13 +20,13 @@
                         <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
                         <ul>
                             <li>
-                                <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
+                                <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>{{ trans('entities.pages_edit_save_draft') }}</a>
                             </li>
                             <li ng-if="isNewPageDraft">
-                                <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
+                                <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('entities.pages_edit_delete_draft') }}</a>
                             </li>
                             <li>
-                                <a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</a>
+                                <a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>{{ trans('entities.pages_edit_discard_draft') }}</a>
                             </li>
                         </ul>
                     </div>
@@ -34,16 +34,16 @@
                 <div class="col-sm-4 faded">
                     <div class="action-buttons" ng-cloak>
                         <div dropdown class="dropdown-container">
-                            <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> @{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}</a>
+                            <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> <span ng-bind="(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || '{{ trans('entities.pages_edit_set_changelog') }}'"></span></a>
                             <ul class="wide">
                                 <li class="padded">
-                                    <p class="text-muted">Enter a brief description of the changes you've made</p>
-                                    <input name="summary" id="summary-input" type="text" placeholder="Enter Changelog" ng-model="changeSummary" />
+                                    <p class="text-muted">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
+                                    <input name="summary" id="summary-input" type="text" placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" ng-model="changeSummary" />
                                 </li>
                             </ul>
                         </div>
 
-                        <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
+                        <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>{{ trans('entities.pages_save') }}</button>
                     </div>
                 </div>
             </div>
@@ -53,7 +53,7 @@
     {{--Title input--}}
     <div class="title-input page-title clearfix" ng-non-bindable>
         <div class="input">
-            @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
+            @include('form/text', ['name' => 'name', 'placeholder' => trans('entities.pages_title')])
         </div>
     </div>
 
@@ -78,24 +78,24 @@
 
                 <div class="markdown-editor-wrap">
                     <div class="editor-toolbar">
-                        <span class="float left">Editor</span>
+                        <span class="float left">{{ trans('entities.pages_md_editor') }}</span>
                         <div class="float right buttons">
-                            <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
+                            <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>{{ trans('entities.pages_md_insert_image') }}</button>
                             &nbsp;|&nbsp;
-                            <button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
+                            <button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>{{ trans('entities.pages_md_insert_link') }}</button>
                         </div>
                     </div>
 
                     <div markdown-input md-change="editorChange" md-model="editContent" class="flex flex-fill">
                         <textarea ng-non-bindable id="markdown-editor-input"  name="markdown" rows="5"
-                                  @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+                            @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
                     </div>
 
                 </div>
 
                 <div class="markdown-editor-wrap">
                     <div class="editor-toolbar">
-                        <div class="">Preview</div>
+                        <div class="">{{ trans('entities.pages_md_preview') }}</div>
                     </div>
                     <div class="markdown-display">
                         <div class="page-content" ng-bind-html="displayContent"></div>
diff --git a/resources/views/pages/guest-create.blade.php b/resources/views/pages/guest-create.blade.php
index 00d9f5560..10e16cb97 100644
--- a/resources/views/pages/guest-create.blade.php
+++ b/resources/views/pages/guest-create.blade.php
@@ -3,19 +3,19 @@
 @section('content')
 
     <div class="container small" ng-non-bindable>
-        <h1>Create Page</h1>
+        <h1>{{ trans('entities.pages_new') }}</h1>
         <form action="{{  $parent->getUrl('/page/create/guest') }}" method="POST">
 
             {!! csrf_field() !!}
 
             <div class="form-group title-input">
-                <label for="name">Page Name</label>
+                <label for="name">{{ trans('entities.pages_name') }}</label>
                 @include('form/text', ['name' => 'name'])
             </div>
 
             <div class="form-group">
-                <a href="{{ $parent->getUrl() }}" class="button muted">Cancel</a>
-                <button type="submit" class="button pos">Continue</button>
+                <a href="{{ $parent->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+                <button type="submit" class="button pos">{{ trans('common.continue') }}</button>
             </div>
 
         </form>
diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/list-item.blade.php
index 7aa5d7933..70b309e7d 100644
--- a/resources/views/pages/list-item.blade.php
+++ b/resources/views/pages/list-item.blade.php
@@ -12,8 +12,7 @@
     @if(isset($style) && $style === 'detailed')
         <div class="row meta text-muted text-small">
             <div class="col-md-6">
-                Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br>
-                Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
+                @include('partials.entity-meta', ['entity' => $page])
             </div>
             <div class="col-md-6">
                 <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
@@ -21,7 +20,7 @@
                 @if($page->chapter)
                     <a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName(30) }}</a>
                 @else
-                    <i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
+                    <i class="zmdi zmdi-collection-bookmark"></i> {{ trans('entities.pages_not_in_chapter') }}
                 @endif
             </div>
         </div>
diff --git a/resources/views/pages/move.blade.php b/resources/views/pages/move.blade.php
index d0fae60ca..a9b6d69d7 100644
--- a/resources/views/pages/move.blade.php
+++ b/resources/views/pages/move.blade.php
@@ -6,34 +6,23 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
-                        @if($page->hasChapter())
-                            <span class="sep">&raquo;</span>
-                            <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
-                                <i class="zmdi zmdi-collection-bookmark"></i>
-                                {{ $page->chapter->getShortName() }}
-                            </a>
-                        @endif
-                        <span class="sep">&raquo;</span>
-                        <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file-text"></i>{{ $page->getShortName() }}</a>
-                    </div>
+                    @include('pages._breadcrumbs', ['page' => $page])
                 </div>
             </div>
         </div>
     </div>
 
     <div class="container">
-        <h1>Move Page <small class="subheader">{{$page->name}}</small></h1>
+        <h1>{{ trans('entities.pages_move') }}</h1>
 
         <form action="{{ $page->getUrl('/move') }}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="PUT">
 
-            @include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
+            @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
 
-            <a href="{{ $page->getUrl() }}" class="button muted">Cancel</a>
-            <button type="submit" class="button pos">Move Page</button>
+            <a href="{{ $page->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button pos">{{ trans('entities.pages_move') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/pages/pdf.blade.php b/resources/views/pages/pdf.blade.php
index 5c9fd5eea..7e43c5e1a 100644
--- a/resources/views/pages/pdf.blade.php
+++ b/resources/views/pages/pdf.blade.php
@@ -36,6 +36,5 @@
             max-width: none;
             display: none;
         }
-
     </style>
 @stop
\ No newline at end of file
diff --git a/resources/views/pages/restrictions.blade.php b/resources/views/pages/restrictions.blade.php
index bd88919df..cfef2ed21 100644
--- a/resources/views/pages/restrictions.blade.php
+++ b/resources/views/pages/restrictions.blade.php
@@ -6,26 +6,15 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
-                        @if($page->hasChapter())
-                            <span class="sep">&raquo;</span>
-                            <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
-                                <i class="zmdi zmdi-collection-bookmark"></i>
-                                {{ $page->chapter->getShortName() }}
-                            </a>
-                        @endif
-                        <span class="sep">&raquo;</span>
-                        <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
-                    </div>
+                    @include('pages._breadcrumbs', ['page' => $page])
                 </div>
             </div>
         </div>
     </div>
 
     <div class="container" ng-non-bindable>
-        <h1>Page Permissions</h1>
-        @include('form/restriction-form', ['model' => $page])
+        <h1>{{ trans('entities.pages_permissions') }}</h1>
+        @include('form.restriction-form', ['model' => $page])
     </div>
 
 @stop
diff --git a/resources/views/pages/revision.blade.php b/resources/views/pages/revision.blade.php
index bc054ef83..fe0dd9511 100644
--- a/resources/views/pages/revision.blade.php
+++ b/resources/views/pages/revision.blade.php
@@ -7,14 +7,12 @@
         <div class="row">
             <div class="col-md-9">
                 <div class="page-content anim fadeIn">
-                    @include('pages/page-display')
+                    @include('pages.page-display')
                 </div>
             </div>
         </div>
     </div>
 
 
-
-    @include('partials/highlight')
-
+    @include('partials.highlight')
 @stop
diff --git a/resources/views/pages/revisions.blade.php b/resources/views/pages/revisions.blade.php
index 720e34fea..3b9812abd 100644
--- a/resources/views/pages/revisions.blade.php
+++ b/resources/views/pages/revisions.blade.php
@@ -6,37 +6,24 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-12 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
-                        @if($page->hasChapter())
-                            <span class="sep">&raquo;</span>
-                            <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
-                                <i class="zmdi zmdi-collection-bookmark"></i>
-                                {{ $page->chapter->getShortName() }}
-                            </a>
-                        @endif
-                        <span class="sep">&raquo;</span>
-                        <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
-                    </div>
+                    @include('pages._breadcrumbs', ['page' => $page])
                 </div>
             </div>
         </div>
     </div>
 
-
-
     <div class="container" ng-non-bindable>
-        <h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
+        <h1>{{ trans('entities.pages_revisions') }}</h1>
 
         @if(count($page->revisions) > 0)
 
             <table class="table">
                 <tr>
-                    <th width="23%">Name</th>
-                    <th colspan="2" width="8%">Created By</th>
-                    <th width="15%">Revision Date</th>
-                    <th width="25%">Changelog</th>
-                    <th width="20%">Actions</th>
+                    <th width="23%">{{ trans('entities.pages_name') }}</th>
+                    <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
+                    <th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
+                    <th width="25%">{{ trans('entities.pages_revisions_changelog') }}</th>
+                    <th width="20%">{{ trans('common.actions') }}</th>
                 </tr>
                 @foreach($page->revisions as $index => $revision)
                     <tr>
@@ -46,19 +33,19 @@
                                 <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
                             @endif
                         </td>
-                        <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
+                        <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
                         <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
                         <td>{{ $revision->summary }}</td>
                         <td>
-                            <a href="{{ $revision->getUrl('changes') }}" target="_blank">Changes</a>
+                            <a href="{{ $revision->getUrl('changes') }}" target="_blank">{{ trans('entities.pages_revisions_changes') }}</a>
                             <span class="text-muted">&nbsp;|&nbsp;</span>
 
                             @if ($index === 0)
-                                <a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a>
+                                <a target="_blank" href="{{ $page->getUrl() }}"><i>{{ trans('entities.pages_revisions_current') }}</i></a>
                             @else
-                                <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
+                                <a href="{{ $revision->getUrl() }}" target="_blank">{{ trans('entities.pages_revisions_preview') }}</a>
                                 <span class="text-muted">&nbsp;|&nbsp;</span>
-                                <a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
+                                <a href="{{ $revision->getUrl('restore') }}">{{ trans('entities.pages_revisions_restore') }}</a>
                             @endif
                         </td>
                     </tr>
@@ -66,7 +53,7 @@
             </table>
 
         @else
-            <p>This page has no revisions.</p>
+            <p>{{ trans('entities.pages_revisions_none') }}</p>
         @endif
 
     </div>
diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php
index 50c6f5d2c..a734b1b95 100644
--- a/resources/views/pages/show.blade.php
+++ b/resources/views/pages/show.blade.php
@@ -6,43 +6,34 @@
         <div class="container">
             <div class="row">
                 <div class="col-sm-6 faded">
-                    <div class="breadcrumbs">
-                        <a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
-                        @if($page->hasChapter())
-                            <span class="sep">&raquo;</span>
-                            <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
-                                <i class="zmdi zmdi-collection-bookmark"></i>
-                                {{ $page->chapter->getShortName() }}
-                            </a>
-                        @endif
-                    </div>
+                    @include('pages._breadcrumbs', ['page' => $page])
                 </div>
                 <div class="col-sm-6 faded">
                     <div class="action-buttons">
                         <span dropdown class="dropdown-container">
-                            <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
+                            <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.pages_export') }}</div>
                             <ul class="wide">
-                                <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
-                                <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
-                                <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
+                                <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.pages_export_html') }} <span class="text-muted float right">.html</span></a></li>
+                                <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.pages_export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
+                                <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.pages_export_text') }} <span class="text-muted float right">.txt</span></a></li>
                             </ul>
                         </span>
                         @if(userCan('page-update', $page))
-                            <a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
+                            <a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>{{ trans('common.edit') }}</a>
                         @endif
                         @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
                             <div dropdown class="dropdown-container">
                                 <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
                                 <ul>
                                     @if(userCan('page-update', $page))
-                                        <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" ><i class="zmdi zmdi-folder"></i>Move</a></li>
-                                        <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary"><i class="zmdi zmdi-replay"></i>Revisions</a></li>
+                                        <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" ><i class="zmdi zmdi-folder"></i>{{ trans('common.move') }}</a></li>
+                                        <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary"><i class="zmdi zmdi-replay"></i>{{ trans('entities.revisions') }}</a></li>
                                     @endif
                                     @if(userCan('restrictions-manage', $page))
-                                        <li><a href="{{ $page->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
+                                        <li><a href="{{ $page->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.permissions') }}</a></li>
                                     @endif
                                     @if(userCan('page-delete', $page))
-                                        <li><a href="{{ $page->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
+                                        <li><a href="{{ $page->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
                                     @endif
                                 </ul>
                             </div>
@@ -64,7 +55,7 @@
                         <div class="pointer anim">
                             <i class="zmdi zmdi-link"></i>
                             <input readonly="readonly" type="text" placeholder="url">
-                            <button class="button icon" title="Copy Link" data-clipboard-text=""><i class="zmdi zmdi-copy"></i></button>
+                            <button class="button icon" title="{{ trans('entities.pages_copy_link') }}" data-clipboard-text=""><i class="zmdi zmdi-copy"></i></button>
                         </div>
                     </div>
 
@@ -72,11 +63,7 @@
 
                     <hr>
 
-                    <p class="text-muted small">
-                        Created {{ $page->created_at->diffForHumans() }} @if($page->createdBy) by <a href="{{ $page->createdBy->getProfileUrl() }}">{{$page->createdBy->name}}</a> @endif
-                        <br>
-                        Last Updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy) by <a href="{{ $page->updatedBy->getProfileUrl() }}">{{$page->updatedBy->name}}</a> @endif
-                    </p>
+                    @include('partials.entity-meta', ['entity' => $page])
 
                 </div>
             </div>
@@ -88,27 +75,27 @@
 
                         @if($book->restricted)
                             @if(userCan('restrictions-manage', $book))
-                                <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
+                                <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}</a>
                             @else
-                                <i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
+                                <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}
                             @endif
                             <br>
                         @endif
 
                         @if($page->chapter && $page->chapter->restricted)
                             @if(userCan('restrictions-manage', $page->chapter))
-                                <a href="{{ $page->chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active</a>
+                                <a href="{{ $page->chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}</a>
                             @else
-                                <i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active
+                                <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}
                             @endif
                             <br>
                         @endif
 
                         @if($page->restricted)
                             @if(userCan('restrictions-manage', $page))
-                                <a href="{{ $page->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Page Permissions Active</a>
+                                <a href="{{ $page->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.pages_permissions_active') }}</a>
                             @else
-                                <i class="zmdi zmdi-lock-outline"></i>Page Permissions Active
+                                <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.pages_permissions_active') }}
                             @endif
                             <br>
                         @endif
diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php
index 5309cb774..0fb97369e 100644
--- a/resources/views/pages/sidebar-tree-list.blade.php
+++ b/resources/views/pages/sidebar-tree-list.blade.php
@@ -18,26 +18,26 @@
     @endif
 
     @if (isset($page) && $page->attachments->count() > 0)
-        <h6 class="text-muted">Attachments</h6>
+        <h6 class="text-muted">{{ trans('entities.pages_attachments') }}</h6>
         @foreach($page->attachments as $attachment)
             <div class="attachment">
-                <a href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif><i class="zmdi zmdi-{{ $attachment->external ? 'open-in-new' : 'file' }}"></i> {{ $attachment->name }}</a>
+                <a href="{{ $attachment->getUrl() }}" @if($attachment->external) target="_blank" @endif><i class="zmdi zmdi-{{ $attachment->external ? 'open-in-new' : 'file' }}"></i>{{ $attachment->name }}</a>
             </div>
         @endforeach
     @endif
 
     @if (isset($pageNav) && $pageNav)
-        <h6 class="text-muted">Page Navigation</h6>
+        <h6 class="text-muted">{{ trans('entities.pages_navigation') }}</h6>
         <div class="sidebar-page-nav menu">
             @foreach($pageNav as $navItem)
-                <li class="page-nav-item {{ $navItem['nodeName'] }}">
+                <li class="page-nav-item h{{ $navItem['level'] }}">
                     <a href="{{ $navItem['link'] }}">{{ $navItem['text'] }}</a>
                 </li>
             @endforeach
         </div>
     @endif
 
-    <h6 class="text-muted">Book Navigation</h6>
+    <h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6>
     <ul class="sidebar-page-list menu">
         <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
 
@@ -50,7 +50,7 @@
 
                 @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
                     <p class="text-muted chapter-toggle @if($bookChild->matchesOrContains($current)) open @endif">
-                        <i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($bookChild->pages) }} Pages</span>
+                        <i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $bookChild->pages->count()]) }}</span>
                     </p>
                     <ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
                         @foreach($bookChild->pages as $childPage)
diff --git a/resources/views/partials/_header-dropdown.blade.php b/resources/views/partials/_header-dropdown.blade.php
new file mode 100644
index 000000000..1c839c9cb
--- /dev/null
+++ b/resources/views/partials/_header-dropdown.blade.php
@@ -0,0 +1,17 @@
+<div class="dropdown-container" dropdown>
+    <span class="user-name" dropdown-toggle>
+        <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
+        <span class="name" ng-non-bindable>{{ $currentUser->getShortName(9) }}</span> <i class="zmdi zmdi-caret-down"></i>
+    </span>
+    <ul>
+        <li>
+            <a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>{{ trans('common.view_profile') }}</a>
+        </li>
+        <li>
+            <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>{{ trans('common.edit_profile') }}</a>
+        </li>
+        <li>
+            <a href="{{ baseUrl('/logout') }}" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>{{ trans('auth.logout') }}</a>
+        </li>
+    </ul>
+</div>
\ No newline at end of file
diff --git a/resources/views/partials/activity-item.blade.php b/resources/views/partials/activity-item.blade.php
index c45ca6c7a..3f162e0bd 100644
--- a/resources/views/partials/activity-item.blade.php
+++ b/resources/views/partials/activity-item.blade.php
@@ -11,7 +11,7 @@
     @if($activity->user)
         <a href="{{ $activity->user->getProfileUrl() }}">{{ $activity->user->name }}</a>
     @else
-        A deleted user
+        {{ trans('common.deleted_user') }}
     @endif
 
     {{ $activity->getText() }}
diff --git a/resources/views/partials/activity-list.blade.php b/resources/views/partials/activity-list.blade.php
index de72f5051..0dc38091e 100644
--- a/resources/views/partials/activity-list.blade.php
+++ b/resources/views/partials/activity-list.blade.php
@@ -8,5 +8,5 @@
         @endforeach
     </div>
 @else
-    <p class="text-muted">No activity to show</p>
+    <p class="text-muted">{{ trans('common.no_activity') }}</p>
 @endif
\ No newline at end of file
diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php
index 885cc2729..62bcc881f 100644
--- a/resources/views/partials/custom-styles.blade.php
+++ b/resources/views/partials/custom-styles.blade.php
@@ -1,4 +1,4 @@
-<style>
+<style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}">
     header, #back-to-top, .primary-background {
         background-color: {{ setting('app-color') }} !important;
     }
diff --git a/resources/views/partials/entity-list.blade.php b/resources/views/partials/entity-list.blade.php
index 412d3be8f..95ebfcd4f 100644
--- a/resources/views/partials/entity-list.blade.php
+++ b/resources/views/partials/entity-list.blade.php
@@ -17,7 +17,7 @@
         @endforeach
     @else
         <p class="text-muted empty-text">
-            {{ $emptyText or 'No items available' }}
+            {{ $emptyText or trans('common.no_items') }}
         </p>
     @endif
 </div>
\ No newline at end of file
diff --git a/resources/views/partials/entity-meta.blade.php b/resources/views/partials/entity-meta.blade.php
new file mode 100644
index 000000000..2a7427422
--- /dev/null
+++ b/resources/views/partials/entity-meta.blade.php
@@ -0,0 +1,13 @@
+<p class="text-muted small">
+    @if ($entity->createdBy)
+        {!! trans('entities.meta_created_name', ['timeLength' => $entity->created_at->diffForHumans(), 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"]) !!}
+    @else
+        {{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}
+    @endif
+    <br>
+    @if ($entity->updatedBy)
+        {!! trans('entities.meta_updated_name', ['timeLength' => $entity->updated_at->diffForHumans(), 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"]) !!}
+    @else
+        {{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}
+    @endif
+</p>
\ No newline at end of file
diff --git a/resources/views/public.blade.php b/resources/views/public.blade.php
index 16aebe2bb..05cf043fd 100644
--- a/resources/views/public.blade.php
+++ b/resources/views/public.blade.php
@@ -25,7 +25,7 @@
 </head>
 <body class="@yield('body-class')" ng-app="bookStack">
 
-@include('partials/notifications')
+@include('partials.notifications')
 
 <header id="header">
     <div class="container">
@@ -47,23 +47,7 @@
                         @yield('header-buttons')
                     </div>
                     @if(isset($signedIn) && $signedIn)
-                        <div class="dropdown-container" dropdown>
-                            <span class="user-name" dropdown-toggle>
-                                <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
-                                <span class="name" ng-non-bindable>{{ $currentUser->getShortName(9) }}</span> <i class="zmdi zmdi-caret-down"></i>
-                            </span>
-                            <ul>
-                                <li>
-                                    <a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>View Profile</a>
-                                </li>
-                                <li>
-                                    <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>Edit Profile</a>
-                                </li>
-                                <li>
-                                    <a href="{{ baseUrl('/logout') }}" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>Logout</a>
-                                </li>
-                            </ul>
-                        </div>
+                        @include('partials._header-dropdown', ['currentUser' => $currentUser])
                     @endif
                 </div>
             </div>
diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php
index fcfb2c902..d4053752f 100644
--- a/resources/views/search/all.blade.php
+++ b/resources/views/search/all.blade.php
@@ -2,49 +2,55 @@
 
 @section('content')
 
-    <div class="container anim fadeIn" ng-non-bindable>
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    <div class="breadcrumbs">
+                        <a href="{{ baseUrl("/search/all?term={$searchTerm}") }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ $searchTerm }}</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
 
-        <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{ $searchTerm }}</span></h1>
+
+    <div class="container" ng-non-bindable>
+
+        <h1>{{ trans('entities.search_results') }}</h1>
 
         <p>
-
             @if(count($pages) > 0)
-                <a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>View all matched pages</a>
+                <a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.search_view_pages') }}</a>
             @endif
 
-
             @if(count($chapters) > 0)
                 &nbsp; &nbsp;&nbsp;
-                <a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>View all matched chapters</a>
+                <a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.search_view_chapters') }}</a>
             @endif
 
             @if(count($books) > 0)
                 &nbsp; &nbsp;&nbsp;
-                <a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>View all matched books</a>
+                <a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.search_view_books') }}</a>
             @endif
         </p>
-        <div class="row">
 
+        <div class="row">
             <div class="col-md-6">
-                <h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">Matching Pages</a></h3>
+                <h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">{{ trans('entities.pages') }}</a></h3>
                 @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
             </div>
-
             <div class="col-md-5 col-md-offset-1">
-
                 @if(count($books) > 0)
-                    <h3><a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="no-color">Matching Books</a></h3>
+                    <h3><a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="no-color">{{ trans('entities.books') }}</a></h3>
                     @include('partials/entity-list', ['entities' => $books])
                 @endif
 
                 @if(count($chapters) > 0)
-                    <h3><a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="no-color">Matching Chapters</a></h3>
+                    <h3><a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="no-color">{{ trans('entities.chapters') }}</a></h3>
                     @include('partials/entity-list', ['entities' => $chapters])
                 @endif
-
             </div>
-
-
         </div>
 
 
diff --git a/resources/views/search/book.blade.php b/resources/views/search/book.blade.php
index 5af2c4d06..0b9119cd5 100644
--- a/resources/views/search/book.blade.php
+++ b/resources/views/search/book.blade.php
@@ -7,7 +7,7 @@
             </div>
         @endforeach
     @else
-        <p class="text-muted">No pages matched this search</p>
+        <p class="text-muted">{{ trans('entities.search_no_pages') }}</p>
     @endif
 </div>
 
diff --git a/resources/views/search/entity-ajax-list.blade.php b/resources/views/search/entity-ajax-list.blade.php
index 97d5e4da7..93f2633aa 100644
--- a/resources/views/search/entity-ajax-list.blade.php
+++ b/resources/views/search/entity-ajax-list.blade.php
@@ -16,7 +16,7 @@
         @endforeach
     @else
         <p class="text-muted">
-            No items available
+            {{ trans('common.no_items') }}
         </p>
     @endif
 </div>
\ No newline at end of file
diff --git a/resources/views/search/entity-search-list.blade.php b/resources/views/search/entity-search-list.blade.php
index dd9c6a509..4e743f6bd 100644
--- a/resources/views/search/entity-search-list.blade.php
+++ b/resources/views/search/entity-search-list.blade.php
@@ -2,17 +2,27 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    <div class="breadcrumbs">
+                        <a href="{{ baseUrl("/search/all?term={$searchTerm}") }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ $searchTerm }}</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container">
         <div class="row">
 
             <div class="col-sm-7">
-                <h1>{{ $title }} <small>{{ $searchTerm }}</small></h1>
+                <h1>{{ $title }}</h1>
                 @include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
                 {!! $entities->links() !!}
             </div>
 
-            <div class="col-sm-4 col-sm-offset-1"></div>
-
         </div>
     </div>
 @stop
\ No newline at end of file
diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php
index ac25eb3b5..c736bc24e 100644
--- a/resources/views/settings/index.blade.php
+++ b/resources/views/settings/index.blade.php
@@ -8,10 +8,10 @@
 
     <h1>{{ trans('settings.settings') }}</h1>
 
-    <form action="{{ baseUrl("/settings") }}" method="POST" ng-cloak>
+    <form action="{{ baseUrl("/settings") }}" method="POST">
         {!! csrf_field() !!}
 
-        <h3>App Settings</h3>
+        <h3>{{ trans('settings.app_settings') }}</h3>
 
         <div class="row">
 
@@ -23,16 +23,16 @@
                 </div>
                 <div class="form-group">
                     <label>{{ trans('settings.app_name_header') }}</label>
-                    <div toggle-switch name="setting-app-name-header" value="{{ setting('app-name-header') }}"></div>
+                    @include('components.toggle-switch', ['name' => 'setting-app-name-header', 'value' => setting('app-name-header')])
                 </div>
                 <div class="form-group">
                     <label for="setting-app-public">{{ trans('settings.app_public_viewing') }}</label>
-                    <div toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></div>
+                    @include('components.toggle-switch', ['name' => 'setting-app-public', 'value' => setting('app-public')])
                 </div>
                 <div class="form-group">
                     <label>{{ trans('settings.app_secure_images') }}</label>
                     <p class="small">{{ trans('settings.app_secure_images_desc') }}</p>
-                    <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div>
+                    @include('components.toggle-switch', ['name' => 'setting-app-secure-images', 'value' => setting('app-secure-images')])
                 </div>
                 <div class="form-group">
                     <label for="setting-app-editor">{{ trans('settings.app_editor') }}</label>
@@ -48,7 +48,18 @@
                 <div class="form-group" id="logo-control">
                     <label for="setting-app-logo">{{ trans('settings.app_logo') }}</label>
                     <p class="small">{!! trans('settings.app_logo_desc') !!}</p>
-                    <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="{{ baseUrl('/logo.png') }}" name="setting-app-logo" image-class="logo-image"></image-picker>
+
+                    @include('components.image-picker', [
+                        'resizeHeight' => '43',
+                        'resizeWidth' => '200',
+                        'showRemove' => true,
+                        'defaultImage' => baseUrl('/logo.png'),
+                        'currentImage' => setting('app-logo'),
+                        'name' => 'setting-app-logo',
+                        'imageClass' => 'logo-image',
+                        'currentId' => false
+                    ])
+
                 </div>
                 <div class="form-group" id="color-control">
                     <label for="setting-app-color">{{ trans('settings.app_primary_color') }}</label>
@@ -74,7 +85,7 @@
             <div class="col-md-6">
                 <div class="form-group">
                     <label for="setting-registration-enabled">{{ trans('settings.reg_allow') }}</label>
-                    <div toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></div>
+                    @include('components.toggle-switch', ['name' => 'setting-registration-enabled', 'value' => setting('registration-enabled')])
                 </div>
                 <div class="form-group">
                     <label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
@@ -91,7 +102,7 @@
                 <div class="form-group">
                     <label for="setting-registration-confirmation">{{ trans('settings.reg_confirm_email') }}</label>
                     <p class="small">{{ trans('settings.reg_confirm_email_desc') }}</p>
-                    <div toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></div>
+                    @include('components.toggle-switch', ['name' => 'setting-registration-confirmation', 'value' => setting('registration-confirmation')])
                 </div>
             </div>
             <div class="col-md-6">
@@ -115,7 +126,7 @@
 
 </div>
 
-@include('partials/image-manager', ['imageType' => 'system'])
+@include('components.image-manager', ['imageType' => 'system'])
 
 @stop
 
@@ -132,10 +143,16 @@
                 var isEmpty = $.trim($elm.val()).length === 0;
                 if (!isEmpty) $elm.val(hexVal);
                 $('#setting-app-color-light').val(isEmpty ? '' : rgbLightVal);
-                // Set page elements to provide preview
-                $('#header, .image-picker .button').attr('style', 'background-color:'+ hexVal+'!important;');
-                $('.faded-small').css('background-color', rgbLightVal);
-                $('.setting-nav a.selected').css('border-bottom-color', hexVal  + '!important');
+
+                var customStyles = document.getElementById('custom-styles');
+                var oldColor = customStyles.getAttribute('data-color');
+                var oldColorLight = customStyles.getAttribute('data-color-light');
+
+                customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal);
+                customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal);
+
+                customStyles.setAttribute('data-color', hexVal);
+                customStyles.setAttribute('data-color-light', rgbLightVal);
             }
         });
     </script>
diff --git a/resources/views/settings/navbar.blade.php b/resources/views/settings/navbar.blade.php
index f4526b4c8..15000b1cf 100644
--- a/resources/views/settings/navbar.blade.php
+++ b/resources/views/settings/navbar.blade.php
@@ -4,13 +4,13 @@
         <div class="row">
             <div class="col-md-12 setting-nav nav-tabs">
                 @if($currentUser->can('settings-manage'))
-                    <a href="{{ baseUrl('/settings') }}" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
+                    <a href="{{ baseUrl('/settings') }}" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>{{ trans('settings.settings') }}</a>
                 @endif
                 @if($currentUser->can('users-manage'))
-                    <a href="{{ baseUrl('/settings/users') }}" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
+                    <a href="{{ baseUrl('/settings/users') }}" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>{{ trans('settings.users') }}</a>
                 @endif
                 @if($currentUser->can('user-roles-manage'))
-                    <a href="{{ baseUrl('/settings/roles') }}" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
+                    <a href="{{ baseUrl('/settings/roles') }}" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>{{ trans('settings.roles') }}</a>
                 @endif
             </div>
         </div>
diff --git a/resources/views/settings/roles/create.blade.php b/resources/views/settings/roles/create.blade.php
index 6a59c0c54..bae42540c 100644
--- a/resources/views/settings/roles/create.blade.php
+++ b/resources/views/settings/roles/create.blade.php
@@ -5,7 +5,7 @@
     @include('settings/navbar', ['selected' => 'roles'])
 
     <div class="container">
-        <h1>Create New Role</h1>
+        <h1>{{ trans('settings.role_create') }}</h1>
 
         <form action="{{ baseUrl("/settings/roles/new") }}" method="POST">
             @include('settings/roles/form')
diff --git a/resources/views/settings/roles/delete.blade.php b/resources/views/settings/roles/delete.blade.php
index 1377d07ae..0b8e1626f 100644
--- a/resources/views/settings/roles/delete.blade.php
+++ b/resources/views/settings/roles/delete.blade.php
@@ -5,8 +5,8 @@
     @include('settings/navbar', ['selected' => 'roles'])
 
     <div class="container small" ng-non-bindable>
-        <h1>Delete Role</h1>
-        <p>This will delete the role with the name '{{ $role->display_name }}'.</p>
+        <h1>{{ trans('settings.role_delete') }}</h1>
+        <p>{{ trans('settings.role_delete_confirm', ['roleName' => $role->display_name]) }}</p>
 
         <form action="{{ baseUrl("/settings/roles/delete/{$role->id}") }}" method="POST">
             {!! csrf_field() !!}
@@ -14,14 +14,14 @@
 
             @if($role->users->count() > 0)
             <div class="form-group">
-                    <p>This role has {{$role->users->count()}} users assigned to it. If you would like to migrate the users from this role select a new role below.</p>
+                    <p>{{ trans('settings.role_delete_users_assigned', ['userCount' => $role->users->count()]) }}</p>
                     @include('form/role-select', ['options' => $roles, 'name' => 'migration_role_id'])
             </div>
             @endif
 
-            <p class="text-neg">Are you sure you want to delete this role?</p>
-            <a href="{{ baseUrl("/settings/roles/{$role->id}") }}" class="button">Cancel</a>
-            <button type="submit" class="button neg">Confirm</button>
+            <p class="text-neg">{{ trans('settings.role_delete_sure') }}</p>
+            <a href="{{ baseUrl("/settings/roles/{$role->id}") }}" class="button muted">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/settings/roles/edit.blade.php b/resources/views/settings/roles/edit.blade.php
index 7e186e898..b46b64663 100644
--- a/resources/views/settings/roles/edit.blade.php
+++ b/resources/views/settings/roles/edit.blade.php
@@ -7,11 +7,11 @@
     <div class="container">
         <div class="row">
             <div class="col-sm-6">
-                <h1>Edit Role <small> {{ $role->display_name }}</small></h1>
+                <h1>{{ trans('settings.role_edit') }}</h1>
             </div>
             <div class="col-sm-6">
                 <p></p>
-                <a href="{{ baseUrl("/settings/roles/delete/{$role->id}") }}" class="button neg float right">Delete Role</a>
+                <a href="{{ baseUrl("/settings/roles/delete/{$role->id}") }}" class="button neg float right">{{ trans('settings.role_delete') }}</a>
             </div>
         </div>
 
diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php
index 78e9e1533..319f3a2df 100644
--- a/resources/views/settings/roles/form.blade.php
+++ b/resources/views/settings/roles/form.blade.php
@@ -5,128 +5,126 @@
     <div class="col-md-9">
         <div class="row">
             <div class="col-md-5">
-                <h3>Role Details</h3>
+                <h3>{{ trans('settings.role_details') }}</h3>
                 <div class="form-group">
-                    <label for="name">Role Name</label>
+                    <label for="name">{{ trans('settings.role_name') }}</label>
                     @include('form/text', ['name' => 'display_name'])
                 </div>
                 <div class="form-group">
-                    <label for="name">Short Role Description</label>
+                    <label for="name">{{ trans('settings.role_desc') }}</label>
                     @include('form/text', ['name' => 'description'])
                 </div>
-                <h3>System Permissions</h3>
-                <label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
-                <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage roles & role permissions</label>
-                <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
-                <label>@include('settings/roles/checkbox', ['permission' => 'permissions']) Manage permissions on own Book, Chapter & Pages</label>
-                <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
+                <h3>{{ trans('settings.role_system') }}</h3>
+                <label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) {{ trans('settings.role_manage_users') }}</label>
+                <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) {{ trans('settings.role_manage_roles') }}</label>
+                <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) {{ trans('settings.role_manage_entity_permissions') }}</label>
+                <label>@include('settings/roles/checkbox', ['permission' => 'permissions']) {{ trans('settings.role_manage_own_entity_permissions') }}</label>
+                <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) {{ trans('settings.role_manage_settings') }}</label>
             </div>
 
             <div class="col-md-6">
 
-                <h3>Asset Permissions</h3>
-                <p>
-                    These permissions control default access to the assets within the system.
-                    Permissions on Books, Chapters and Pages will override these permissions.
-                </p>
+                <h3>{{ trans('settings.role_asset') }}</h3>
+                <p>{{ trans('settings.role_asset_desc') }}</p>
+
                 <table class="table">
                     <tr>
                         <th width="20%"></th>
-                        <th width="20%">Create</th>
-                        <th width="20%">View</th>
-                        <th width="20%">Edit</th>
-                        <th width="20%">Delete</th>
+                        <th width="20%">{{ trans('common.create') }}</th>
+                        <th width="20%">{{ trans('common.view') }}</th>
+                        <th width="20%">{{ trans('common.edit') }}</th>
+                        <th width="20%">{{ trans('common.delete') }}</th>
                     </tr>
                     <tr>
-                        <td>Books</td>
+                        <td>{{ trans('entities.books') }}</td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-view-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-view-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-view-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-view-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                     </tr>
                     <tr>
-                        <td>Chapters</td>
+                        <td>{{ trans('entities.chapters') }}</td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                     </tr>
                     <tr>
-                        <td>Pages</td>
+                        <td>{{ trans('entities.pages') }}</td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-view-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-view-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-view-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-view-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                     </tr>
                     <tr>
-                        <td>Images</td>
+                        <td>{{ trans('entities.images') }}</td>
                         <td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
-                        <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
+                        <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                     </tr>
                     <tr>
-                        <td>Attachments</td>
+                        <td>{{ trans('entities.attachments') }}</td>
                         <td>@include('settings/roles/checkbox', ['permission' => 'attachment-create-all'])</td>
-                        <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
+                        <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-update-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                         <td>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-own']) Own</label>
-                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) All</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-own']) {{ trans('settings.role_own') }}</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) {{ trans('settings.role_all') }}</label>
                         </td>
                     </tr>
                 </table>
             </div>
         </div>
-        <a href="{{ baseUrl("/settings/roles") }}" class="button muted">Cancel</a>
-        <button type="submit" class="button pos">Save Role</button>
+        <a href="{{ baseUrl("/settings/roles") }}" class="button muted">{{ trans('common.cancel') }}</a>
+        <button type="submit" class="button pos">{{ trans('settings.role_save') }}</button>
     </div>
     <div class="col-md-3">
-        <h3>Users in this role</h3>
+        <h3>{{ trans('settings.role_users') }}</h3>
 
         @if(isset($role) && count($role->users) > 0)
         <table class="list-table">
@@ -147,7 +145,7 @@
         </table>
         @else
             <p class="text-muted">
-                No users currently in this role.
+                {{ trans('settings.role_users_none') }}
             </p>
         @endif
 
diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php
index 2c905ae22..c24254946 100644
--- a/resources/views/settings/roles/index.blade.php
+++ b/resources/views/settings/roles/index.blade.php
@@ -8,25 +8,25 @@
 
         <div class="row action-header">
             <div class="col-sm-8">
-                <h1>User Roles</h1>
+                <h1>{{ trans('settings.role_user_roles') }}</h1>
             </div>
             <div class="col-sm-4">
                 <p></p>
-                <a href="{{ baseUrl("/settings/roles/new") }}" class="button float right pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a>
+                <a href="{{ baseUrl("/settings/roles/new") }}" class="button float right pos"><i class="zmdi zmdi-lock-open"></i>{{ trans('settings.role_create') }}</a>
             </div>
         </div>
 
         <table class="table">
             <tr>
-                <th>Role Name</th>
+                <th>{{ trans('settings.role_name') }}</th>
                 <th></th>
-                <th class="text-right">Users</th>
+                <th class="text-center">{{ trans('settings.users') }}</th>
             </tr>
             @foreach($roles as $role)
                 <tr>
                     <td><a href="{{ baseUrl("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
                     <td>{{ $role->description }}</td>
-                    <td class="text-right">{{ $role->users->count() }}</td>
+                    <td class="text-center">{{ $role->users->count() }}</td>
                 </tr>
             @endforeach
         </table>
diff --git a/resources/views/users/create.blade.php b/resources/views/users/create.blade.php
index b25f2ee8a..bff79820a 100644
--- a/resources/views/users/create.blade.php
+++ b/resources/views/users/create.blade.php
@@ -3,6 +3,18 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    <div class="breadcrumbs">
+                        <a href="{{ baseUrl('/settings/users') }}" class="text-button"><i class="zmdi zmdi-accounts"></i>Users</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
         <h1>Create User</h1>
 
diff --git a/resources/views/users/delete.blade.php b/resources/views/users/delete.blade.php
index 74b993b3c..135231a32 100644
--- a/resources/views/users/delete.blade.php
+++ b/resources/views/users/delete.blade.php
@@ -2,16 +2,30 @@
 
 @section('content')
 
+    <div class="faded-small toolbar">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm-12 faded">
+                    <div class="breadcrumbs">
+                        <a href="{{ baseUrl("/settings/users") }}" class="text-button"><i class="zmdi zmdi-accounts"></i>Users</a>
+                        <span class="sep">&raquo;</span>
+                        <a href="{{ baseUrl("/settings/users/{$user->id}") }}" class="text-button"><i class="zmdi zmdi-account"></i>{{ $user->name }}</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="container small" ng-non-bindable>
-        <h1>Delete User</h1>
-        <p>This will fully delete this user with the name '<span class="text-neg">{{ $user->name }}</span>' from the system.</p>
-        <p class="text-neg">Are you sure you want to delete this user?</p>
+        <h1>{{ trans('settings.users_delete') }}</h1>
+        <p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
+        <p class="text-neg">{{ trans('settings.users_delete_confirm') }}</p>
 
         <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="POST">
             {!! csrf_field() !!}
             <input type="hidden" name="_method" value="DELETE">
-            <a href="{{ baseUrl("/settings/users/{$user->id}") }}" class="button muted">Cancel</a>
-            <button type="submit" class="button neg">Confirm</button>
+            <a href="{{ baseUrl("/settings/users/{$user->id}") }}" class="button muted">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
         </form>
     </div>
 
diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php
index 6cbbdb7f7..4db00e31f 100644
--- a/resources/views/users/edit.blade.php
+++ b/resources/views/users/edit.blade.php
@@ -11,12 +11,12 @@
         <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post">
             <div class="row">
                 <div class="col-sm-8">
-                    <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
+                    <h1>{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
                 </div>
                 <div class="col-sm-4">
                     <p></p>
                     @if($authMethod !== 'system')
-                        <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
+                        <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">{{ trans('settings.users_delete') }}</a>
                     @endif
                 </div>
             </div>
@@ -29,9 +29,20 @@
             </div>
             <div class="col-md-6">
                 <div class="form-group" id="logo-control">
-                    <label for="user-avatar">User Avatar</label>
-                    <p class="small">This image should be approx 256px square.</p>
-                    <image-picker resize-height="512" resize-width="512" current-image="{{ $user->getAvatar(80) }}" current-id="{{ $user->image_id }}" default-image="{{ baseUrl("/user_avatar.png") }}" name="image_id" show-remove="false" image-class="['avatar' ,'large']"></image-picker>
+                    <label for="user-avatar">{{ trans('settings.users_avatar') }}</label>
+                    <p class="small">{{ trans('settings.users_avatar_desc') }}</p>
+
+                    @include('components.image-picker', [
+                          'resizeHeight' => '512',
+                          'resizeWidth' => '512',
+                          'showRemove' => false,
+                          'defaultImage' => baseUrl('/user_avatar.png'),
+                          'currentImage' => $user->getAvatar(80),
+                          'currentId' => $user->image_id,
+                          'name' => 'image_id',
+                          'imageClass' => 'avatar large'
+                      ])
+
                 </div>
             </div>
         </div>
@@ -40,20 +51,17 @@
         <hr class="margin-top large">
 
         @if($currentUser->id === $user->id && count($activeSocialDrivers) > 0)
-            <h3>Social Accounts</h3>
-            <p class="text-muted">
-                Here you can connect your other accounts for quicker and easier login. <br>
-                Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.
-            </p>
+            <h3>{{ trans('settings.users_social_accounts') }}</h3>
+            <p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
             <div class="row">
                 @if(isset($activeSocialDrivers['google']))
                     <div class="col-md-3 text-center">
                         <div><i class="zmdi zmdi-google-plus-box zmdi-hc-4x" style="color: #DC4E41;"></i></div>
                         <div>
                             @if($user->hasSocialAccount('google'))
-                                <a href="{{ baseUrl("/login/service/google/detach") }}" class="button neg">Disconnect Account</a>
+                                <a href="{{ baseUrl("/login/service/google/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
                             @else
-                                <a href="{{ baseUrl("/login/service/google") }}" class="button pos">Attach Account</a>
+                                <a href="{{ baseUrl("/login/service/google") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
                             @endif
                         </div>
                     </div>
@@ -63,9 +71,9 @@
                         <div><i class="zmdi zmdi-github zmdi-hc-4x" style="color: #444;"></i></div>
                         <div>
                             @if($user->hasSocialAccount('github'))
-                                <a href="{{ baseUrl("/login/service/github/detach") }}" class="button neg">Disconnect Account</a>
+                                <a href="{{ baseUrl("/login/service/github/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
                             @else
-                                <a href="{{ baseUrl("/login/service/github") }}" class="button pos">Attach Account</a>
+                                <a href="{{ baseUrl("/login/service/github") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
                             @endif
                         </div>
                     </div>
@@ -77,5 +85,5 @@
     </div>
 
     <p class="margin-top large"><br></p>
-    @include('partials/image-manager', ['imageType' => 'user'])
+    @include('components.image-manager', ['imageType' => 'user'])
 @stop
diff --git a/resources/views/users/forms/ldap.blade.php b/resources/views/users/forms/ldap.blade.php
index 6e4d2a5fd..0a6cf79ae 100644
--- a/resources/views/users/forms/ldap.blade.php
+++ b/resources/views/users/forms/ldap.blade.php
@@ -1,30 +1,30 @@
 <div class="form-group">
-    <label for="name">Name</label>
+    <label for="name">{{ trans('auth.name') }}</label>
     @include('form.text', ['name' => 'name'])
 </div>
 
 @if(userCan('users-manage'))
 <div class="form-group">
-    <label for="email">Email</label>
+    <label for="email">{{ trans('auth.email') }}</label>
     @include('form.text', ['name' => 'email'])
 </div>
 @endif
 
 @if(userCan('users-manage'))
     <div class="form-group">
-        <label for="role">User Role</label>
+        <label for="role">{{ trans('settings.users_role') }}</label>
         @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
     </div>
 @endif
 
 @if(userCan('users-manage'))
     <div class="form-group">
-        <label for="external_auth_id">External Authentication ID</label>
+        <label for="external_auth_id">{{ trans('settings.users_external_auth_id') }}</label>
         @include('form.text', ['name' => 'external_auth_id'])
     </div>
 @endif
 
 <div class="form-group">
-    <a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
-    <button class="button pos" type="submit">Save</button>
+    <a href="{{ baseUrl("/settings/users") }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button class="button pos" type="submit">{{ trans('common.save') }}</button>
 </div>
\ No newline at end of file
diff --git a/resources/views/users/forms/standard.blade.php b/resources/views/users/forms/standard.blade.php
index d902c5b2c..39ae4c770 100644
--- a/resources/views/users/forms/standard.blade.php
+++ b/resources/views/users/forms/standard.blade.php
@@ -1,16 +1,16 @@
 <div class="form-group">
-    <label for="name">Name</label>
+    <label for="name">{{ trans('auth.name') }}</label>
     @include('form.text', ['name' => 'name'])
 </div>
 
 <div class="form-group">
-    <label for="email">Email</label>
+    <label for="email">{{ trans('auth.email') }}</label>
     @include('form.text', ['name' => 'email'])
 </div>
 
 @if(userCan('users-manage'))
     <div class="form-group">
-        <label for="role">User Role</label>
+        <label for="role">{{ trans('settings.users_role') }}</label>
         @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
     </div>
 @endif
@@ -18,23 +18,23 @@
 @if(isset($model))
     <div class="form-group">
         <span class="text-muted">
-            Only fill the below if you would like <br>to change your password:
+            {{ trans('settings.users_password_warning') }}
         </span>
     </div>
 @endif
 
 <div class="form-group">
-    <label for="password">Password</label>
+    <label for="password">{{ trans('auth.password') }}</label>
     @include('form.password', ['name' => 'password'])
 </div>
 
 <div class="form-group">
-    <label for="password-confirm">Confirm Password</label>
+    <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
     @include('form.password', ['name' => 'password-confirm'])
 </div>
 
 <div class="form-group">
-    <a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
-    <button class="button pos" type="submit">Save</button>
+    <a href="{{ baseUrl("/settings/users") }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button class="button pos" type="submit">{{ trans('common.save') }}</button>
 </div>
 
diff --git a/resources/views/users/forms/system.blade.php b/resources/views/users/forms/system.blade.php
index 3ee5f6409..868dad854 100644
--- a/resources/views/users/forms/system.blade.php
+++ b/resources/views/users/forms/system.blade.php
@@ -1,25 +1,26 @@
 @if($user->system_name == 'public')
-    <p>This user represents any guest users that visit your instance. It cannot be used for logins but is assigned&nbsp;automatically.</p>
+    <p>{{ trans('settings.users_system_public') }}</p>
 @endif
 
 <div class="form-group">
-    <label for="name">Name</label>
+    <label for="name">{{ trans('auth.name') }}</label>
     @include('form.text', ['name' => 'name'])
 </div>
 
 <div class="form-group">
-    <label for="email">Email</label>
+    <label for="email">{{ trans('auth.email') }}</label>
     @include('form.text', ['name' => 'email'])
 </div>
 
 @if(userCan('users-manage'))
     <div class="form-group">
-        <label for="role">User Role</label>
+        <label for="role">{{ trans('settings.users_role') }}</label>
         @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
     </div>
 @endif
 
 <div class="form-group">
-    <a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
-    <button class="button pos" type="submit">Save</button>
+    <a href="{{ baseUrl("/settings/users") }}" class="button muted">{{ trans('common.cancel') }}</a>
+    <button class="button pos" type="submit">{{ trans('common.save') }}</button>
 </div>
+
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php
index 105fddb5b..3ea5a03f7 100644
--- a/resources/views/users/index.blade.php
+++ b/resources/views/users/index.blade.php
@@ -9,12 +9,12 @@
     <div class="container small" ng-non-bindable>
         <div class="row action-header">
             <div class="col-sm-8">
-                <h1>Users</h1>
+                <h1>{{ trans('settings.users') }}</h1>
             </div>
             <div class="col-sm-4">
                 <p></p>
                 @if(userCan('users-manage'))
-                    <a href="{{ baseUrl("/settings/users/create") }}" class="pos button float right"><i class="zmdi zmdi-account-add"></i>Add new user</a>
+                    <a href="{{ baseUrl("/settings/users/create") }}" class="pos button float right"><i class="zmdi zmdi-account-add"></i>{{ trans('settings.users_add_new') }}</a>
                 @endif
             </div>
         </div>
@@ -30,20 +30,17 @@
                     @foreach(collect($listDetails)->except('search') as $name => $val)
                         <input type="hidden" name="{{ $name }}" value="{{ $val }}">
                     @endforeach
-                    <input type="text" name="search" placeholder="Search Users" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
+                    <input type="text" name="search" placeholder="{{ trans('settings.users_search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
                 </form>
             </div>
         </div>
-        <div class="text-center">
-
-        </div>
 
         <table class="table">
             <tr>
                 <th></th>
-                <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'name']) }}">Name</a></th>
-                <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">Email</a></th>
-                <th>User Roles</th>
+                <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'name']) }}">{{ trans('auth.name') }}</a></th>
+                <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">{{ trans('auth.email') }}</a></th>
+                <th>{{ trans('settings.role_user_roles') }}</th>
             </tr>
             @foreach($users as $user)
                 <tr>
diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php
index 1ae869979..10bd0ec63 100644
--- a/resources/views/users/profile.blade.php
+++ b/resources/views/users/profile.blade.php
@@ -17,21 +17,21 @@
                             <div>
                                 <h3 style="margin-top: 0;">{{ $user->name }}</h3>
                                 <p class="text-muted">
-                                    User for {{ $user->created_at->diffForHumans(null, true) }}
+                                    {{ trans('entities.profile_user_for_x', ['time' => $user->created_at->diffForHumans(null, true)]) }}
                                 </p>
                             </div>
                         </div>
                     </div>
                     <div class="col-md-5 text-bigger" id="content-counts">
-                        <div class="text-muted">Created Content</div>
+                        <div class="text-muted">{{ trans('entities.profile_created_content') }}</div>
                         <div class="text-book">
-                            <i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ $assetCounts['books'] }} {{ str_plural('Book', $assetCounts['books']) }}
+                            <i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ $assetCounts['books'] }} {{ str_plural(trans('entities.book'), $assetCounts['books']) }}
                         </div>
                         <div class="text-chapter">
-                            <i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ $assetCounts['chapters'] }} {{ str_plural('Chapter', $assetCounts['chapters']) }}
+                            <i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ $assetCounts['chapters'] }} {{ str_plural(trans('entities.chapter'), $assetCounts['chapters']) }}
                         </div>
                         <div class="text-page">
-                            <i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ $assetCounts['pages'] }} {{ str_plural('Page', $assetCounts['pages']) }}
+                            <i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ $assetCounts['pages'] }} {{ str_plural(trans('entities.page'), $assetCounts['pages']) }}
                         </div>
                     </div>
                 </div>
@@ -39,34 +39,34 @@
 
                 <hr class="even">
 
-                <h3>Recently Created Pages</h3>
+                <h3>{{ trans('entities.recently_created_pages') }}</h3>
                 @if (count($recentlyCreated['pages']) > 0)
                     @include('partials/entity-list', ['entities' => $recentlyCreated['pages']])
                 @else
-                    <p class="text-muted">{{ $user->name }} has not created any pages</p>
+                    <p class="text-muted">{{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}</p>
                 @endif
 
                 <hr class="even">
 
-                <h3>Recently Created Chapters</h3>
+                <h3>{{ trans('entities.recently_created_chapters') }}</h3>
                 @if (count($recentlyCreated['chapters']) > 0)
                     @include('partials/entity-list', ['entities' => $recentlyCreated['chapters']])
                 @else
-                    <p class="text-muted">{{ $user->name }} has not created any chapters</p>
+                    <p class="text-muted">{{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}</p>
                 @endif
 
                 <hr class="even">
 
-                <h3>Recently Created Books</h3>
+                <h3>{{ trans('entities.recently_created_books') }}</h3>
                 @if (count($recentlyCreated['books']) > 0)
                     @include('partials/entity-list', ['entities' => $recentlyCreated['books']])
                 @else
-                    <p class="text-muted">{{ $user->name }} has not created any books</p>
+                    <p class="text-muted">{{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}</p>
                 @endif
             </div>
 
             <div class="col-sm-4 col-sm-offset-1" id="recent-activity">
-                <h3>Recent Activity</h3>
+                <h3>{{ trans('entities.recent_activity') }}</h3>
                 @include('partials/activity-list', ['activity' => $activity])
             </div>
 
diff --git a/resources/views/vendor/notifications/email-plain.blade.php b/resources/views/vendor/notifications/email-plain.blade.php
index acefa6523..7ca1dc8d0 100644
--- a/resources/views/vendor/notifications/email-plain.blade.php
+++ b/resources/views/vendor/notifications/email-plain.blade.php
@@ -18,5 +18,5 @@ if (! empty($outroLines)) {
     echo implode("\n", $outroLines), "\n\n";
 }
 
-echo 'Regards,', "\n";
-echo config('app.name'), "\n";
+echo "\n";
+echo setting('app-name'), "\n";
diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php
index 297c6be05..3e02cbba9 100644
--- a/resources/views/vendor/notifications/email.blade.php
+++ b/resources/views/vendor/notifications/email.blade.php
@@ -159,8 +159,7 @@ $style = [
                                                             <tr>
                                                                 <td style="{{ $fontFamily }}">
                                                                     <p style="{{ $style['paragraph-sub'] }}">
-                                                                        If you’re having trouble clicking the "{{ $actionText }}" button,
-                                                                        copy and paste the URL below into your web browser:
+                                                                        {{ trans('common.email_action_help', ['actionText' => $actionText]) }}
                                                                     </p>
 
                                                                     <p style="{{ $style['paragraph-sub'] }}">
@@ -188,7 +187,7 @@ $style = [
                                                     <p style="{{ $style['paragraph-sub'] }}">
                                                         &copy; {{ date('Y') }}
                                                         <a style="{{ $style['anchor'] }}" href="{{ baseUrl('/') }}" target="_blank">{{ setting('app-name') }}</a>.
-                                                        All rights reserved.
+                                                        {{ trans('common.email_rights') }}
                                                     </p>
                                                 </td>
                                             </tr>
diff --git a/routes/web.php b/routes/web.php
index d179c28a5..076ffb94f 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,5 +1,7 @@
 <?php
 
+Route::get('/translations.js', 'HomeController@getTranslations');
+
 // Authenticated routes...
 Route::group(['middleware' => 'auth'], function () {
 
@@ -107,7 +109,6 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
         Route::get('/suggest/names', 'TagController@getNameSuggestions');
         Route::get('/suggest/values', 'TagController@getValueSuggestions');
-        Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
     });
 
     Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php
index 0d2e4ac17..664ec297a 100644
--- a/tests/Auth/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -24,7 +24,7 @@ class AuthTest extends TestCase
         $settings->put('app-public', 'true');
         $this->visit('/')
             ->seePageIs('/')
-            ->see('Sign In');
+            ->see('Log In');
     }
 
     public function test_registration_showing()
@@ -132,7 +132,7 @@ class AuthTest extends TestCase
 
         $this->asAdmin()
             ->visit('/settings/users')
-            ->click('Add new user')
+            ->click('Add New User')
             ->type($user->name, '#name')
             ->type($user->email, '#email')
             ->check('roles[admin]')
@@ -245,7 +245,7 @@ class AuthTest extends TestCase
     {
         $this->setSettings(['registration-enabled' => 'true']);
         $this->visit('/password/email')
-            ->seeLink('Sign in')
+            ->seeLink('Log in')
             ->seeLink('Sign up');
     }
 
@@ -260,6 +260,6 @@ class AuthTest extends TestCase
         return $this->visit('/login')
             ->type($email, '#email')
             ->type($password, '#password')
-            ->press('Sign In');
+            ->press('Log In');
     }
 }
diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php
index 9573321fb..45e46fe81 100644
--- a/tests/Auth/LdapTest.php
+++ b/tests/Auth/LdapTest.php
@@ -36,11 +36,11 @@ class LdapTest extends \TestCase
             ->see('Username')
             ->type($this->mockUser->name, '#username')
             ->type($this->mockUser->password, '#password')
-            ->press('Sign In')
+            ->press('Log In')
             ->seePageIs('/login')->see('Please enter an email to use for this account.');
 
         $this->type($this->mockUser->email, '#email')
-            ->press('Sign In')
+            ->press('Log In')
             ->seePageIs('/')
             ->see($this->mockUser->name)
             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
@@ -64,7 +64,7 @@ class LdapTest extends \TestCase
             ->see('Username')
             ->type($this->mockUser->name, '#username')
             ->type($this->mockUser->password, '#password')
-            ->press('Sign In')
+            ->press('Log In')
             ->seePageIs('/')
             ->see($this->mockUser->name)
             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
@@ -87,7 +87,7 @@ class LdapTest extends \TestCase
             ->see('Username')
             ->type($this->mockUser->name, '#username')
             ->type($this->mockUser->password, '#password')
-            ->press('Sign In')
+            ->press('Log In')
             ->seePageIs('/login')->see('These credentials do not match our records.')
             ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
     }
diff --git a/tests/Auth/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php
index d5a7e6921..5739f9a7d 100644
--- a/tests/Auth/SocialAuthTest.php
+++ b/tests/Auth/SocialAuthTest.php
@@ -32,4 +32,48 @@ class SocialAuthTest extends TestCase
         $this->seeInDatabase('social_accounts', ['user_id' => $user->id]);
     }
 
+    public function test_social_login()
+    {
+        $user = factory(\BookStack\User::class)->make();
+
+        config([
+            'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
+            'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
+            'APP_URL' => 'http://localhost'
+        ]);
+
+        $mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
+        $this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
+        $mockSocialDriver = Mockery::mock('Laravel\Socialite\Contracts\Provider');
+        $mockSocialUser = Mockery::mock('\Laravel\Socialite\Contracts\User');
+
+        $mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123');
+
+        $mockSocialDriver->shouldReceive('user')->twice()->andReturn($mockSocialUser);
+        $mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);
+        $mockSocialite->shouldReceive('driver')->twice()->with('github')->andReturn($mockSocialDriver);
+        $mockSocialDriver->shouldReceive('redirect')->twice()->andReturn(redirect('/'));
+
+        // Test login routes
+        $this->visit('/login')->seeElement('#social-login-google')
+            ->click('#social-login-google')
+            ->seePageIs('/login');
+
+        // Test social callback
+        $this->visit('/login/service/google/callback')->seePageIs('/login')
+            ->see(trans('errors.social_account_not_used', ['socialAccount' => 'Google']));
+
+        $this->visit('/login')->seeElement('#social-login-github')
+        ->click('#social-login-github')
+        ->seePageIs('/login');
+
+        // Test social callback with matching social account
+        DB::table('social_accounts')->insert([
+            'user_id' => $this->getAdmin()->id,
+            'driver' => 'github',
+            'driver_id' => 'logintest123'
+        ]);
+        $this->visit('/login/service/github/callback')->seePageIs('/');
+    }
+
 }
diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php
index 60b5ceebd..3f60f9c36 100644
--- a/tests/Entity/EntitySearchTest.php
+++ b/tests/Entity/EntitySearchTest.php
@@ -15,8 +15,8 @@ class EntitySearchTest extends TestCase
             ->type($page->name, 'term')
             ->press('header-search-box-button')
             ->see('Search Results')
-            ->see($page->name)
-            ->click($page->name)
+            ->seeInElement('.entity-list', $page->name)
+            ->clickInElement('.entity-list', $page->name)
             ->seePageIs($page->getUrl());
     }
 
diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php
index 20721968f..9fd4eb9ad 100644
--- a/tests/Entity/EntityTest.php
+++ b/tests/Entity/EntityTest.php
@@ -136,7 +136,7 @@ class EntityTest extends TestCase
         $this->asAdmin()
             ->visit('/books')
             // Choose to create a book
-            ->click('Add new book')
+            ->click('Create New Book')
             ->seePageIs('/books/create')
             // Fill out form & save
             ->type($book->name, '#name')
@@ -168,7 +168,7 @@ class EntityTest extends TestCase
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($creator);
         app('BookStack\Repos\UserRepo')->destroy($creator);
-        app('BookStack\Repos\PageRepo')->saveRevision($entities['page']);
+        app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']);
 
         $this->checkEntitiesViewable($entities);
     }
@@ -181,7 +181,7 @@ class EntityTest extends TestCase
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($updater);
         app('BookStack\Repos\UserRepo')->destroy($updater);
-        app('BookStack\Repos\PageRepo')->saveRevision($entities['page']);
+        app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']);
 
         $this->checkEntitiesViewable($entities);
     }
diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php
index 1a46e30bc..233f300ee 100644
--- a/tests/Entity/PageDraftTest.php
+++ b/tests/Entity/PageDraftTest.php
@@ -4,13 +4,13 @@
 class PageDraftTest extends TestCase
 {
     protected $page;
-    protected $pageRepo;
+    protected $entityRepo;
 
     public function setUp()
     {
         parent::setUp();
         $this->page = \BookStack\Page::first();
-        $this->pageRepo = app('\BookStack\Repos\PageRepo');
+        $this->entityRepo = app('\BookStack\Repos\EntityRepo');
     }
 
     public function test_draft_content_shows_if_available()
@@ -20,7 +20,7 @@ class PageDraftTest extends TestCase
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
-        $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
+        $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]);
         $this->asAdmin()->visit($this->page->getUrl() . '/edit')
             ->seeInField('html', $newContent);
     }
@@ -33,7 +33,7 @@ class PageDraftTest extends TestCase
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
-        $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
+        $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]);
         $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
             ->dontSeeInField('html', $newContent);
     }
@@ -41,7 +41,7 @@ class PageDraftTest extends TestCase
     public function test_alert_message_shows_if_editing_draft()
     {
         $this->asAdmin();
-        $this->pageRepo->saveUpdateDraft($this->page, ['html' => 'test content']);
+        $this->entityRepo->updatePageDraft($this->page, ['html' => 'test content']);
         $this->asAdmin()->visit($this->page->getUrl() . '/edit')
             ->see('You are currently editing a draft');
     }
@@ -55,7 +55,7 @@ class PageDraftTest extends TestCase
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
-        $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
+        $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]);
 
         $this->actingAs($newUser)
             ->visit($this->page->getUrl() . '/edit')
diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php
index 80783912a..4784297a2 100644
--- a/tests/Entity/SortTest.php
+++ b/tests/Entity/SortTest.php
@@ -13,8 +13,8 @@ class SortTest extends TestCase
     public function test_drafts_do_not_show_up()
     {
         $this->asAdmin();
-        $pageRepo = app('\BookStack\Repos\PageRepo');
-        $draft = $pageRepo->getDraftPage($this->book);
+        $entityRepo = app('\BookStack\Repos\EntityRepo');
+        $draft = $entityRepo->getDraftPage($this->book);
 
         $this->visit($this->book->getUrl())
             ->see($draft->name)
@@ -28,7 +28,7 @@ class SortTest extends TestCase
         $currentBook = $page->book;
         $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
         $this->asAdmin()->visit($page->getUrl() . '/move')
-            ->see('Move Page')->see($page->name)
+            ->see('Move Page')
             ->type('book:' . $newBook->id, 'entity_selection')->press('Move Page');
 
         $page = \BookStack\Page::find($page->id);
@@ -48,7 +48,7 @@ class SortTest extends TestCase
         $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
 
         $this->asAdmin()->visit($chapter->getUrl() . '/move')
-            ->see('Move Chapter')->see($chapter->name)
+            ->see('Move Chapter')
             ->type('book:' . $newBook->id, 'entity_selection')->press('Move Chapter');
 
         $chapter = \BookStack\Chapter::find($chapter->id);
diff --git a/tests/ImageTest.php b/tests/ImageTest.php
index 031517cdb..9da12d36b 100644
--- a/tests/ImageTest.php
+++ b/tests/ImageTest.php
@@ -90,7 +90,7 @@ class ImageTest extends TestCase
             'type' => 'gallery'
         ]);
 
-        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has been deleted');
+        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected');
     }
 
 }
\ No newline at end of file
diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php
index d3830cff7..cddd3206a 100644
--- a/tests/Permissions/RestrictionsTest.php
+++ b/tests/Permissions/RestrictionsTest.php
@@ -65,9 +65,9 @@ class RestrictionsTest extends TestCase
         $this->forceVisit($bookUrl)
             ->see('Book not found');
         $this->forceVisit($bookPage->getUrl())
-            ->see('Book not found');
+            ->see('Page not found');
         $this->forceVisit($bookChapter->getUrl())
-            ->see('Book not found');
+            ->see('Chapter not found');
 
         $this->setEntityRestrictions($book, ['view']);
 
diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php
index 7a0515fd9..500dd3b67 100644
--- a/tests/Permissions/RolesTest.php
+++ b/tests/Permissions/RolesTest.php
@@ -81,7 +81,7 @@ class RolesTest extends TestCase
         $this->asAdmin()->visit('/settings')
             ->click('Roles')
             ->seePageIs('/settings/roles')
-            ->click('Add new role')
+            ->click('Create New Role')
             ->type('Test Role', 'display_name')
             ->type('A little test description', 'description')
             ->press('Save Role')
@@ -211,7 +211,7 @@ class RolesTest extends TestCase
         $this->checkAccessPermission('book-create-all', [
             '/books/create'
         ], [
-            '/books' => 'Add new book'
+            '/books' => 'Create New Book'
         ]);
 
         $this->visit('/books/create')
diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php
index 685146423..2ea5fbfed 100644
--- a/tests/PublicActionTest.php
+++ b/tests/PublicActionTest.php
@@ -64,7 +64,7 @@ class PublicActionTest extends TestCase
         $this->visit($chapter->book->getUrl());
         $this->visit($chapter->getUrl())
             ->click('New Page')
-            ->see('Create Page')
+            ->see('New Page')
             ->seePageIs($chapter->getUrl('/create-page'));
 
         $this->submitForm('Continue', [