From d50bec8caa0c1e834e8ed1d906a530d14a86513b Mon Sep 17 00:00:00 2001
From: Henrique Dias <hacdias@gmail.com>
Date: Tue, 1 Aug 2017 20:49:56 +0100
Subject: [PATCH] Internationalization (#183)

* update dependencies to latest version

* add mising dependencies

* Syntax updates and such

* Reorganize files and translate login to portuguese

* Add i18n to buttons

* Error translations and some bug fixes

* Add i18n to files

* i18n on prompts

* update search

* Prompts and Sidebar in

* i18n to the header

* Change to YAML

* alphabetical order

* # Add simplified Chinese language (#180)

* Add Simplified Chinese and sort by alphabet

* Add more text to translations

* API Updates

* Update zh_cn.yaml (#182)

* Api Upgrades

* Simplify api and clean zh_cn lang file

* Improve error logging

* Fix some route bugs and separate login styles

* better organization

* Fix bug on api

* Build assets Tue, Aug  1, 2017 11:32:23 AM

* Rename users path and fix bug scroll event

* Start Portuguese translation and file org

* Add more to the PT translation

* Add show

* Build assets Tue Aug  1 12:01:39 GMTST 2017

* Add locale to cofnig

* Update portuguese translation

* You can change the language :)

* :D

* Build assets Tue Aug  1 17:50:31 GMTST 2017

* Update requestContext variable names

* Remove assets

* Build assets Tue Aug  1 20:48:21 GMTST 2017


Former-commit-id: 08f373725c14990f61dbb00bea43118c496c5d32 [formerly 281e23007c79dac1e9b86424201891a99d20f73a] [formerly b1b73f42debbce06b4f36e4cf97e319789c85b9f [formerly d8bc73390c37409efa60804d94779a7629944caa]]
Former-commit-id: 92e99405cbf9935d1cf77b0fe70b122fca552be6 [formerly 3cd365e862f2a54ada60e226a19ac607b8d0c43b]
Former-commit-id: cf9815114ac686cdf75a6b1cba15adafe493d083
---
 assets/build/webpack.base.conf.js             |   4 +
 assets/index.html                             |   5 +-
 assets/src/components/Header.vue              |  16 +-
 assets/src/components/Languages.vue           |  19 ++
 assets/src/components/Login.vue               | 118 ---------
 assets/src/components/ProfileSettings.vue     |  82 ------
 assets/src/components/Search.vue              |  16 +-
 assets/src/components/Sidebar.vue             |  24 +-
 assets/src/components/buttons/Copy.vue        |   4 +-
 assets/src/components/buttons/Delete.vue      |   4 +-
 assets/src/components/buttons/Download.vue    |   4 +-
 assets/src/components/buttons/Info.vue        |   4 +-
 assets/src/components/buttons/Move.vue        |   4 +-
 assets/src/components/buttons/Rename.vue      |   4 +-
 assets/src/components/buttons/SwitchView.vue  |   4 +-
 assets/src/components/buttons/Upload.vue      |   4 +-
 assets/src/components/{ => files}/Editor.vue  |   8 +-
 assets/src/components/{ => files}/Listing.vue |  22 +-
 .../components/{ => files}/ListingItem.vue    |   0
 assets/src/components/{ => files}/Preview.vue |  20 +-
 assets/src/components/prompts/Copy.vue        |  11 +-
 assets/src/components/prompts/Delete.vue      |  13 +-
 assets/src/components/prompts/Download.vue    |   5 +-
 assets/src/components/prompts/Error.vue       |   6 +-
 assets/src/components/prompts/FileList.vue    |   2 +-
 assets/src/components/prompts/Help.vue        |  27 +-
 assets/src/components/prompts/Info.vue        |  24 +-
 assets/src/components/prompts/Move.vue        |  11 +-
 assets/src/components/prompts/NewDir.vue      |  11 +-
 assets/src/components/prompts/NewFile.vue     |  11 +-
 assets/src/components/prompts/Prompts.vue     |   5 +-
 assets/src/components/prompts/Rename.vue      |  12 +-
 assets/src/components/prompts/Success.vue     |   2 +-
 assets/src/css/dashboard.css                  |  33 ++-
 assets/src/css/login.css                      |  68 +++++
 assets/src/css/styles.css                     |   1 +
 assets/src/i18n/en.yaml                       | 164 ++++++++++++
 assets/src/i18n/index.js                      |  19 ++
 assets/src/i18n/pt.yaml                       | 165 ++++++++++++
 assets/src/i18n/zh-cn.yaml                    | 151 +++++++++++
 assets/src/main.js                            |   2 +
 assets/src/router/index.js                    |  54 ++--
 assets/src/store/mutations.js                 |   8 +-
 assets/src/utils/api.js                       | 243 +++++++-----------
 assets/src/utils/auth.js                      |   6 +-
 assets/src/{components => views}/Files.vue    |  69 +++--
 .../{components => views}/GlobalSettings.vue  |  56 ++--
 .../{components/Main.vue => views/Layout.vue} |  10 +-
 assets/src/views/Login.vue                    |  42 +++
 assets/src/views/ProfileSettings.vue          | 103 ++++++++
 assets/src/{components => views}/User.vue     |  99 +++----
 assets/src/{components => views}/Users.vue    |   8 +-
 .../src/{components => views}/errors/403.vue  |   2 +-
 .../src/{components => views}/errors/404.vue  |   2 +-
 .../src/{components => views}/errors/500.vue  |   2 +-
 auth.go                                       |   8 +-
 cmd/filemanager/main.go                       |   5 +
 download.go                                   |  12 +-
 file.go                                       |   2 +-
 filemanager.go                                |  36 ++-
 http.go                                       |  34 ++-
 package.json                                  |  34 +--
 plugins/hugo.go                               |   6 +-
 resource.go                                   |  12 +-
 rice-box.go.REMOVED.git-id                    |   2 +-
 settings.go                                   | 159 ++++++------
 users.go                                      | 214 ++++++++-------
 67 files changed, 1450 insertions(+), 887 deletions(-)
 create mode 100644 assets/src/components/Languages.vue
 delete mode 100644 assets/src/components/Login.vue
 delete mode 100644 assets/src/components/ProfileSettings.vue
 rename assets/src/components/{ => files}/Editor.vue (96%)
 rename assets/src/components/{ => files}/Listing.vue (93%)
 rename assets/src/components/{ => files}/ListingItem.vue (100%)
 rename assets/src/components/{ => files}/Preview.vue (79%)
 create mode 100644 assets/src/css/login.css
 create mode 100644 assets/src/i18n/en.yaml
 create mode 100644 assets/src/i18n/index.js
 create mode 100644 assets/src/i18n/pt.yaml
 create mode 100644 assets/src/i18n/zh-cn.yaml
 rename assets/src/{components => views}/Files.vue (83%)
 rename assets/src/{components => views}/GlobalSettings.vue (75%)
 rename assets/src/{components/Main.vue => views/Layout.vue} (84%)
 create mode 100644 assets/src/views/Login.vue
 create mode 100644 assets/src/views/ProfileSettings.vue
 rename assets/src/{components => views}/User.vue (64%)
 rename assets/src/{components => views}/Users.vue (74%)
 rename assets/src/{components => views}/errors/403.vue (78%)
 rename assets/src/{components => views}/errors/404.vue (77%)
 rename assets/src/{components => views}/errors/500.vue (79%)

diff --git a/assets/build/webpack.base.conf.js b/assets/build/webpack.base.conf.js
index 5016698b..f8188022 100644
--- a/assets/build/webpack.base.conf.js
+++ b/assets/build/webpack.base.conf.js
@@ -25,6 +25,10 @@ module.exports = {
   },
   module: {
     rules: [
+      {
+        test: /\.(yml|yaml)$/,
+        loader: 'yml-loader'
+      },
       {
         test: /\.(js|vue)$/,
         loader: 'eslint-loader',
diff --git a/assets/index.html b/assets/index.html
index 31f4cab4..a0922521 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -21,11 +21,10 @@
   <!-- Add to home screen for Windows -->
   <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
   <meta name="msapplication-TileColor" content="#2979ff">
-
-  <% for (var chunk of webpack.chunks) {
+  <% for (var chunk of webpack.compilation.chunks) {
     for (var file of chunk.files) {
       if (file.match(/\.(js|css)$/)) { %>
-      <link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
+      <link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
 
   <!-- Plugins info -->
   <script>{{ .JavaScript }}</script>
diff --git a/assets/src/components/Header.vue b/assets/src/components/Header.vue
index 9f9b2a77..656d8361 100644
--- a/assets/src/components/Header.vue
+++ b/assets/src/components/Header.vue
@@ -1,19 +1,19 @@
 <template>
   <header>
     <div>
-      <button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
+      <button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
         <i class="material-icons">menu</i>
       </button>
       <img src="../assets/logo.svg" alt="File Manager">
       <search></search>
     </div>
     <div>
-      <button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
+      <button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
         <i class="material-icons">search</i>
       </button>
 
-      <button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
-        <i class="material-icons" title="Save">save</i>
+      <button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
+        <i class="material-icons">save</i>
       </button>
 
       <div v-for="plugin in plugins" :key="plugin.name">
@@ -30,7 +30,7 @@
         </button>
       </div>
 
-      <button @click="openMore" id="more" aria-label="More" title="More" class="action">
+      <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
         <i class="material-icons">more_vert</i>
       </button>
 
@@ -71,9 +71,9 @@
         <upload-button v-show="showUpload"></upload-button>
         <info-button v-show="showCommonButton"></info-button>
 
-        <button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action">
+        <button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
           <i class="material-icons">check_circle</i>
-          <span>Select</span>
+          <span>{{ $t('buttons.select') }}</span>
         </button>
       </div>
       <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
@@ -92,7 +92,7 @@ import SwitchButton from './buttons/SwitchView'
 import MoveButton from './buttons/Move'
 import CopyButton from './buttons/Copy'
 import {mapGetters, mapState} from 'vuex'
-import api from '@/utils/api'
+import * as api from '@/utils/api'
 import buttons from '@/utils/buttons'
 
 export default {
diff --git a/assets/src/components/Languages.vue b/assets/src/components/Languages.vue
new file mode 100644
index 00000000..a8374223
--- /dev/null
+++ b/assets/src/components/Languages.vue
@@ -0,0 +1,19 @@
+<template>
+  <select v-on:change="change" :value="selected">
+    <option value="en">{{ $t('languages.en') }}</option>
+    <option value="pt">{{ $t('languages.pt') }}</option>
+    <option value="zh-cn">{{ $t('languages.zhCN') }}</option>
+  </select>
+</template>
+
+<script>
+export default {
+  name: 'languages',
+  props: [ 'selected' ],
+  methods: {
+    change (event) {
+      this.$emit('update:selected', event.target.value)
+    }
+  }
+}
+</script>
diff --git a/assets/src/components/Login.vue b/assets/src/components/Login.vue
deleted file mode 100644
index 62a642f8..00000000
--- a/assets/src/components/Login.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-  <div id="login">
-    <form @submit="submit">
-      <img src="../assets/logo.svg" alt="File Manager">
-      <h1>File Manager</h1>
-      <div v-if="wrong" class="wrong">Wrong credentials</div>
-      <input type="text" v-model="username" placeholder="Username">
-      <input type="password" v-model="password" placeholder="Password">
-      <input type="submit" value="Login">
-    </form>
-  </div>
-</template>
-
-<script>
-import auth from '@/utils/auth'
-
-export default {
-  name: 'login',
-  data: function () {
-    return {
-      wrong: false,
-      username: '',
-      password: ''
-    }
-  },
-  methods: {
-    submit: function (event) {
-      event.preventDefault()
-      event.stopPropagation()
-
-      let redirect = this.$route.query.redirect
-      if (redirect === '' || redirect === undefined || redirect === null) {
-        redirect = '/files/'
-      }
-
-      auth.login(this.username, this.password)
-        .then(() => {
-          this.$router.push({ path: redirect })
-        })
-        .catch(() => {
-          this.wrong = true
-        })
-    }
-  }
-}
-</script>
-
-<style>
-#login {
-  background: #fff;
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-}
-
-#login img {
-  width: 4em;
-  height: 4em;
-  margin: 0 auto;
-  display: block;
-}
-
-#login h1 {
-  text-align: center;
-  font-size: 2.5em;
-  margin: .4em 0 .67em;
-}
-
-#login form {
-  position: fixed;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  max-width: 16em;
-  width: 90%;
-}
-
-#login input {
-  width: 100%;
-  width: 100%;
-  margin: .5em 0 0;
-}
-
-#login .wrong {
-  background: #F44336;
-  color: #fff;
-  padding: .5em;
-  text-align: center;
-  animation: .2s opac forwards;
-}
-
-@keyframes opac {
-  0% {
-    opacity: 0;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
-#login input[type="text"],
-#login input[type="password"] {
-  padding: .5em 1em;
-  border: 1px solid #e9e9e9;
-  transition: .2s ease border;
-  color: #333;
-}
-
-#login input[type="text"]:focus,
-#login input[type="password"]:focus,
-#login input[type="text"]:hover,
-#login input[type="password"]:hover {
-  border-color: #9f9f9f;
-}
-</style>
-
diff --git a/assets/src/components/ProfileSettings.vue b/assets/src/components/ProfileSettings.vue
deleted file mode 100644
index ccb70947..00000000
--- a/assets/src/components/ProfileSettings.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-  <div class="dashboard">
-    <h1>Profile Settings</h1>
-
-    <ul v-if="user.admin">
-      <li><router-link to="/settings/global">Go to Global Settings</router-link></li>
-    </ul>
-
-    <form @submit="changePassword">
-      <h2>Change Password</h2>
-      <p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
-      <p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
-      <p><input type="submit" value="Change Password"></p>
-    </form>
-
-    <form @submit="updateCSS">
-      <h2>Custom Stylesheet</h2>
-      <textarea v-model="css" name="css"></textarea>
-      <p><input type="submit" value="Update"></p>
-    </form>
-  </div>
-</template>
-
-<script>
-import { mapState, mapMutations } from 'vuex'
-import api from '@/utils/api'
-
-export default {
-  name: 'settings',
-  data: function () {
-    return {
-      password: '',
-      passwordConf: '',
-      css: ''
-    }
-  },
-  computed: {
-    ...mapState([ 'user' ]),
-    passwordClass () {
-      if (this.password === '' && this.passwordConf === '') {
-        return ''
-      }
-
-      if (this.password === this.passwordConf) {
-        return 'green'
-      }
-
-      return 'red'
-    }
-  },
-  created () {
-    this.css = this.user.css
-  },
-  methods: {
-    ...mapMutations([ 'showSuccess' ]),
-    changePassword (event) {
-      event.preventDefault()
-
-      if (this.password !== this.passwordConf) {
-        return
-      }
-
-      api.updatePassword(this.password).then(() => {
-        this.showSuccess('Password updated!')
-      }).catch(e => {
-        this.$store.commit('showError', e)
-      })
-    },
-    updateCSS (event) {
-      event.preventDefault()
-
-      api.updateCSS(this.css).then(() => {
-        this.$store.commit('setUserCSS', this.css)
-        this.$emit('css-updated')
-        this.showSuccess('Styles updated!')
-      }).catch(e => {
-        this.$store.commit('showError', e)
-      })
-    }
-  }
-}
-</script>
diff --git a/assets/src/components/Search.vue b/assets/src/components/Search.vue
index 369d2b59..6829597f 100644
--- a/assets/src/components/Search.vue
+++ b/assets/src/components/Search.vue
@@ -1,7 +1,7 @@
 <template>
   <div id="search" @click="open" v-bind:class="{ active , ongoing }">
     <div id="input">
-      <button v-if="active" class="action" @click="close">
+      <button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
         <i class="material-icons">arrow_back</i>
       </button>
       <i v-else class="material-icons">search</i>
@@ -11,7 +11,7 @@
         ref="input"
         :autofocus="active"
         v-model.trim="value"
-        aria-label="Write here to search"
+        :aria-label="$t('search.writeToSearch')"
         :placeholder="placeholder">
     </div>
 
@@ -78,10 +78,10 @@ export default {
     // Placeholder value.
     placeholder: function () {
       if (this.user.allowCommands && this.user.commands.length > 0) {
-        return 'Search or execute a command...'
+        return this.$t('search.searchOrCommand')
       }
 
-      return 'Search...'
+      return this.$t('search.search')
     },
     // The text that is shown on the results' box while
     // there is no search result or command output to show.
@@ -92,16 +92,16 @@ export default {
 
       if (this.value.length === 0) {
         if (this.user.allowCommands && this.user.commands.length > 0) {
-          return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
+          return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
         }
 
-        return 'Type and press enter to search.'
+        this.$t('search.type')
       }
 
       if (!this.supported() || !this.user.allowCommands) {
-        return 'Press enter to search.'
+        return this.$t('search.pressToSearch')
       } else {
-        return 'Press enter to execute.'
+        return this.$t('search.pressToExecute')
       }
     }
   },
diff --git a/assets/src/components/Sidebar.vue b/assets/src/components/Sidebar.vue
index cd462a94..87b9f40b 100644
--- a/assets/src/components/Sidebar.vue
+++ b/assets/src/components/Sidebar.vue
@@ -1,19 +1,19 @@
 <template>
   <nav :class="{active}">
-    <router-link class="action" to="/files/" aria-label="My Files" title="My Files">
+    <router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
       <i class="material-icons">folder</i>
-      <span>My Files</span>
+      <span>{{ $t('sidebar.myFiles') }}</span>
     </router-link>
 
     <div v-if="user.allowNew">
-      <button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
+      <button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
         <i class="material-icons">create_new_folder</i>
-        <span>New folder</span>
+        <span>{{ $t('sidebar.newFolder') }}</span>
       </button>
 
-      <button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
+      <button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
         <i class="material-icons">note_add</i>
-        <span>New file</span>
+        <span>{{ $t('sidebar.newFile') }}</span>
       </button>
     </div>
 
@@ -25,21 +25,21 @@
     </div>
 
     <div>
-      <router-link class="action" to="/settings" aria-label="Settings" title="Settings">
+      <router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
         <i class="material-icons">settings_applications</i>
-        <span>Settings</span>
+        <span>{{ $t('sidebar.settings') }}</span>
       </router-link>
 
-      <button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
+      <button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
         <i class="material-icons">exit_to_app</i>
-        <span>Logout</span>
+        <span>{{ $t('sidebar.logout') }}</span>
       </button>
     </div>
 
     <p class="credits">
-      <span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
+      <span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
       <span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
-      <span><a @click="help">Help</a></span>
+      <span><a @click="help">{{ $t('sidebar.help') }}</a></span>
     </p>
   </nav>
 </template>
diff --git a/assets/src/components/buttons/Copy.vue b/assets/src/components/buttons/Copy.vue
index 9ee554a4..164cf347 100644
--- a/assets/src/components/buttons/Copy.vue
+++ b/assets/src/components/buttons/Copy.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
+  <button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
     <i class="material-icons">content_copy</i>
-    <span>Copy file</span>
+    <span>{{ $t('buttons.copyFile') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/Delete.vue b/assets/src/components/buttons/Delete.vue
index 69c43c6e..bb3c0fdf 100644
--- a/assets/src/components/buttons/Delete.vue
+++ b/assets/src/components/buttons/Delete.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="show" aria-label="Delete"  title="Delete" class="action" id="delete-button">
+  <button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
     <i class="material-icons">delete</i>
-    <span>Delete</span>
+    <span>{{ $t('buttons.delete') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/Download.vue b/assets/src/components/buttons/Download.vue
index d2bf4a0c..788d746e 100644
--- a/assets/src/components/buttons/Download.vue
+++ b/assets/src/components/buttons/Download.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="download" aria-label="Download" title="Download" id="download-button" class="action">
+  <button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
     <i class="material-icons">file_download</i>
-    <span>Download</span>
+    <span>{{ $t('buttons.download') }}</span>
     <span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
   </button>
 </template>
diff --git a/assets/src/components/buttons/Info.vue b/assets/src/components/buttons/Info.vue
index 6e2ef91f..164e59c9 100644
--- a/assets/src/components/buttons/Info.vue
+++ b/assets/src/components/buttons/Info.vue
@@ -1,7 +1,7 @@
 <template>
-  <button title="Info" aria-label="Info" class="action" @click="show">
+  <button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
     <i class="material-icons">info</i>
-    <span>Info</span>
+    <span>{{ $t('buttons.info') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/Move.vue b/assets/src/components/buttons/Move.vue
index a7b5834d..5d9250f4 100644
--- a/assets/src/components/buttons/Move.vue
+++ b/assets/src/components/buttons/Move.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
+  <button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
     <i class="material-icons">forward</i>
-    <span>Move file</span>
+    <span>{{ $t('buttons.moveFile') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/Rename.vue b/assets/src/components/buttons/Rename.vue
index 9b2922b0..a2fd2cbf 100644
--- a/assets/src/components/buttons/Rename.vue
+++ b/assets/src/components/buttons/Rename.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
+  <button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
     <i class="material-icons">mode_edit</i>
-    <span>Rename</span>
+    <span>{{ $t('buttons.rename') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/SwitchView.vue b/assets/src/components/buttons/SwitchView.vue
index d4e3fba2..48b1fb1c 100644
--- a/assets/src/components/buttons/SwitchView.vue
+++ b/assets/src/components/buttons/SwitchView.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
+  <button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
     <i class="material-icons">{{ icon() }}</i>
-    <span>Switch view</span>
+    <span>{{ $t('buttons.switchView') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/buttons/Upload.vue b/assets/src/components/buttons/Upload.vue
index a2922ea9..f5d738ad 100644
--- a/assets/src/components/buttons/Upload.vue
+++ b/assets/src/components/buttons/Upload.vue
@@ -1,7 +1,7 @@
 <template>
-  <button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
+  <button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
     <i class="material-icons">file_upload</i>
-    <span>Upload</span>
+    <span>{{ $t('buttons.upload') }}</span>
   </button>
 </template>
 
diff --git a/assets/src/components/Editor.vue b/assets/src/components/files/Editor.vue
similarity index 96%
rename from assets/src/components/Editor.vue
rename to assets/src/components/files/Editor.vue
index af789b1d..2d0f41bb 100644
--- a/assets/src/components/Editor.vue
+++ b/assets/src/components/files/Editor.vue
@@ -1,10 +1,10 @@
 <template>
     <form id="editor" :class="req.language">
         <div v-if="hasMetadata" id="metadata">
-          <h2>Metadata</h2>
+          <h2>{{ $t('files.metadata') }}</h2>
         </div>
 
-        <h2 v-if="hasMetadata">Body</h2>
+        <h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
     </form>
 </template>
 
@@ -123,7 +123,3 @@ export default {
   }
 }
 </script>
-
-<style>
-
-</style>
diff --git a/assets/src/components/Listing.vue b/assets/src/components/files/Listing.vue
similarity index 93%
rename from assets/src/components/Listing.vue
rename to assets/src/components/files/Listing.vue
index 3abddcf6..8eb7497f 100644
--- a/assets/src/components/Listing.vue
+++ b/assets/src/components/files/Listing.vue
@@ -2,9 +2,9 @@
   <div v-if="(req.numDirs + req.numFiles) == 0">
     <h2 class="message">
       <i class="material-icons">sentiment_dissatisfied</i>
-      <span>It feels lonely here...</span>
+      <span>{{ $t('files.lonely') }}</span>
     </h2>
-    <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
+    <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
   </div>
   <div v-else id="listing"
     :class="req.display"
@@ -16,23 +16,23 @@
         <div></div>
         <div>
           <p :class="{ active: nameSorted }" class="name" @click="sort('name')">
-            <span>Name</span>
+            <span>{{ $t('files.name') }}</span>
             <i class="material-icons">{{ nameIcon }}</i>
           </p>
 
           <p :class="{ active: sizeSorted }" class="size" @click="sort('size')">
-            <span>Size</span>
+            <span>{{ $t('files.size') }}</span>
             <i class="material-icons">{{ sizeIcon }}</i>
           </p>
           <p :class="{ active: modifiedSorted }" class="modified" @click="sort('modified')">
-            <span>Last modified</span>
+            <span>{{ $t('files.lastModified') }}</span>
             <i class="material-icons">{{ modifiedIcon }}</i>
           </p>
         </div>
       </div>
     </div>
 
-    <h2 v-if="req.numDirs > 0">Folders</h2>
+    <h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
     <div v-if="req.numDirs > 0">
       <item v-for="(item, index) in req.items"
         v-if="item.isDir"
@@ -47,7 +47,7 @@
       </item>
     </div>
 
-    <h2 v-if="req.numFiles > 0">Files</h2>
+    <h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
     <div v-if="req.numFiles > 0">
       <item v-for="(item, index) in req.items"
         v-if="!item.isDir"
@@ -62,12 +62,12 @@
       </item>
     </div>
 
-    <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
+    <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
 
     <div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
-    <p>Multiple selection enabled</p>
-      <div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
-        <i class="material-icons" title="Clear">clear</i>
+    <p>{{ $t('files.multipleSelectionEnabled') }}</p>
+      <div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
+        <i class="material-icons">clear</i>
       </div>
     </div>
   </div>
diff --git a/assets/src/components/ListingItem.vue b/assets/src/components/files/ListingItem.vue
similarity index 100%
rename from assets/src/components/ListingItem.vue
rename to assets/src/components/files/ListingItem.vue
diff --git a/assets/src/components/Preview.vue b/assets/src/components/files/Preview.vue
similarity index 79%
rename from assets/src/components/Preview.vue
rename to assets/src/components/files/Preview.vue
index 142cf2ba..454eb981 100644
--- a/assets/src/components/Preview.vue
+++ b/assets/src/components/files/Preview.vue
@@ -1,7 +1,7 @@
 <template>
   <div id="previewer">
     <div class="bar">
-      <button @click="back" class="action" aria-label="Close Preview" id="close">
+      <button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
         <i class="material-icons">close</i>
       </button>
 
@@ -11,8 +11,12 @@
       <info-button></info-button>
     </div>
 
-    <button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
-    <button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
+    <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
+      <i class="material-icons">chevron_left</i>
+    </button>
+    <button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
+      <i class="material-icons">chevron_right</i>
+    </button>
 
     <div class="preview">
       <img v-if="req.type == 'image'" :src="raw()">
@@ -24,7 +28,7 @@
       </video>
       <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
       <a v-else-if="req.type == 'blob'" :href="download()">
-        <h2 class="message">Download <i class="material-icons">file_download</i></h2>
+        <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
       </a>
       <pre v-else >{{ req.content }}</pre>
     </div>
@@ -35,10 +39,10 @@
 import { mapState } from 'vuex'
 import url from '@/utils/url'
 import api from '@/utils/api'
-import InfoButton from './buttons/Info'
-import DeleteButton from './buttons/Delete'
-import RenameButton from './buttons/Rename'
-import DownloadButton from './buttons/Download'
+import InfoButton from '@/components/buttons/Info'
+import DeleteButton from '@/components/buttons/Delete'
+import RenameButton from '@/components/buttons/Rename'
+import DownloadButton from '@/components/buttons/Download'
 
 export default {
   name: 'preview',
diff --git a/assets/src/components/prompts/Copy.vue b/assets/src/components/prompts/Copy.vue
index 184182f6..b4cb403c 100644
--- a/assets/src/components/prompts/Copy.vue
+++ b/assets/src/components/prompts/Copy.vue
@@ -1,13 +1,16 @@
 <template>
   <div class="prompt">
-    <h3>Copy</h3>
-    <p>Choose the place to copy your files:</p>
+    <h3>{{ $t('prompts.copy') }}</h3>
+    <p>{{ $t('prompts.copyMessage') }}</p>
 
     <file-list @update:selected="val => dest = val"></file-list>
 
     <div>
-      <button class="ok" @click="copy">Copy</button>
-      <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
+      <button class="ok" @click="copy">{{ $t('buttons.copy') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Delete.vue b/assets/src/components/prompts/Delete.vue
index e0cba26e..3e14fbd0 100644
--- a/assets/src/components/prompts/Delete.vue
+++ b/assets/src/components/prompts/Delete.vue
@@ -1,11 +1,14 @@
 <template>
   <div class="prompt">
-    <h3>Delete files</h3>
-    <p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
-    <p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
+    <h3>{{ $t('prompts.deleteTitle') }}</h3>
+    <p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
+    <p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
     <div>
-      <button @click="submit" autofocus>Delete</button>
-      <button @click="closeHovers" class="cancel">Cancel</button>
+      <button @click="submit" autofocus>{{ $t('buttons.delete') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Download.vue b/assets/src/components/prompts/Download.vue
index 18b401ba..8e89ea58 100644
--- a/assets/src/components/prompts/Download.vue
+++ b/assets/src/components/prompts/Download.vue
@@ -1,7 +1,8 @@
 <template>
   <div class="prompt" id="download">
-    <h3>Download files</h3>
-    <p>Choose the format you want to download.</p>
+    <h3>{{ $t('prompts.download') }}</h3>
+    <p>{{ $t('prompts.downloadMessage') }}</p>
+
     <button @click="download('zip')" autofocus>zip</button>
     <button @click="download('tar')" autofocus>tar</button>
     <button @click="download('targz')" autofocus>tar.gz</button>
diff --git a/assets/src/components/prompts/Error.vue b/assets/src/components/prompts/Error.vue
index 2312eae8..c5e0ea49 100644
--- a/assets/src/components/prompts/Error.vue
+++ b/assets/src/components/prompts/Error.vue
@@ -1,11 +1,11 @@
 <template>
   <div class="prompt error">
     <i class="material-icons">error_outline</i>
-    <h3>Something went wrong</h3>
+    <h3>{{ $t('prompts.error') }}</h3>
     <pre>{{ $store.state.showMessage }}</pre>
     <div>
-      <button @click="close" autofocus>Close</button>
-      <button @click="reportIssue" class="cancel">Report Issue</button>
+      <button @click="close" autofocus>{{ $t('buttons.close') }}</button>
+      <button @click="reportIssue" class="cancel">{{ $t('buttons.reportIssue') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/FileList.vue b/assets/src/components/prompts/FileList.vue
index 52864096..eaa29645 100644
--- a/assets/src/components/prompts/FileList.vue
+++ b/assets/src/components/prompts/FileList.vue
@@ -9,7 +9,7 @@
         :data-url="item.url">{{ item.name }}</li>
     </ul>
 
-    <p>Currently navigating on: <code>{{ nav }}</code>.</p>
+    <p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
   </div>
 </template>
 
diff --git a/assets/src/components/prompts/Help.vue b/assets/src/components/prompts/Help.vue
index 454e76bf..fbf88282 100644
--- a/assets/src/components/prompts/Help.vue
+++ b/assets/src/components/prompts/Help.vue
@@ -1,26 +1,21 @@
 <template>
   <div class="prompt help">
-    <h3>Help</h3>
+    <h3>{{ $t('help.help') }}</h3>
 
     <ul>
-      <li><strong>F1</strong> - this information</li>
-      <li><strong>F2</strong> - rename file</li>
-      <li><strong>DEL</strong> - delete selected items</li>
-      <li><strong>ESC</strong> - clear selection and/or close the prompt</li>
-      <li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
-      <li><strong>CTRL + Click</strong> - select multiple files or directories</li>
-      <li><strong>Double click</strong> - open a file or directory</li>
-      <li><strong>Click</strong> - select file or directory</li>
-    </ul>
-
-    <p>Not available yet</p>
-
-    <ul>
-      <li><strong>Alt + Click</strong> - select a group of files</li>
+      <li><strong>F1</strong> - {{ $t('help.f1') }}</li>
+      <li><strong>F2</strong> - {{ $t('help.f2') }}</li>
+      <li><strong>DEL</strong> - {{ $t('help.del') }}</li>
+      <li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
+      <li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
+      <li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
+      <li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
+      <li><strong>Click</strong> - {{ $t('help.click') }}</li>
+      <li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
     </ul>
 
     <div>
-      <button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
+      <button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Info.vue b/assets/src/components/prompts/Info.vue
index 8f625e43..2752bb75 100644
--- a/assets/src/components/prompts/Info.vue
+++ b/assets/src/components/prompts/Info.vue
@@ -1,27 +1,27 @@
 <template>
   <div class="prompt">
-    <h3>File Information</h3>
+    <h3>{{ $t('prompts.fileInfo') }}</h3>
 
-    <p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
+    <p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
 
-    <p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
-    <p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
-    <p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
+    <p v-show="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
+    <p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
+    <p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
 
     <section v-show="dir() && selected.length === 0">
-      <p><strong>Number of files:</strong> {{ req.numFiles }}</p>
-      <p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
+      <p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
+      <p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
     </section>
 
     <section v-show="!dir()">
-      <p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
-      <p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
-      <p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
-      <p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
+      <p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
+      <p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
+      <p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
+      <p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
     </section>
 
     <div>
-      <button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
+      <button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Move.vue b/assets/src/components/prompts/Move.vue
index ebfb7ad2..be6f42cd 100644
--- a/assets/src/components/prompts/Move.vue
+++ b/assets/src/components/prompts/Move.vue
@@ -1,13 +1,16 @@
 <template>
   <div class="prompt">
-    <h3>Move</h3>
-    <p>Choose new house for your file(s)/folder(s):</p>
+    <h3>{{ $t('prompts.move') }}</h3>
+    <p>{{ $t('prompts.moveMessage') }}</p>
 
     <file-list @update:selected="val => dest = val"></file-list>
 
     <div>
-      <button class="ok" @click="move">Move</button>
-      <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
+      <button class="ok" @click="move">{{ $t('buttons.move') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/NewDir.vue b/assets/src/components/prompts/NewDir.vue
index bee602fd..9b33263e 100644
--- a/assets/src/components/prompts/NewDir.vue
+++ b/assets/src/components/prompts/NewDir.vue
@@ -1,11 +1,14 @@
 <template>
   <div class="prompt">
-    <h3>New directory</h3>
-    <p>Write the name of the new directory.</p>
+    <h3>{{ $t('prompts.newDir') }}</h3>
+    <p>{{ $t('prompts.newDirMessage') }}</p>
     <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
     <div>
-      <button class="ok" @click="submit">Create</button>
-      <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
+      <button class="ok" @click="submit">{{ $t('buttons.create') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/NewFile.vue b/assets/src/components/prompts/NewFile.vue
index c40e2844..d9b6c333 100644
--- a/assets/src/components/prompts/NewFile.vue
+++ b/assets/src/components/prompts/NewFile.vue
@@ -1,11 +1,14 @@
 <template>
   <div class="prompt">
-    <h3>New file</h3>
-    <p>Write the name of the new file.</p>
+    <h3>{{ $t('prompts.newFile') }}</h3>
+    <p>{{ $t('prompts.newFileMessage') }}</p>
     <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
     <div>
-      <button class="ok" @click="submit">Create</button>
-      <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
+      <button class="ok" @click="submit">{{ $t('buttons.create') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Prompts.vue b/assets/src/components/prompts/Prompts.vue
index a78e3c78..0551bd7a 100644
--- a/assets/src/components/prompts/Prompts.vue
+++ b/assets/src/components/prompts/Prompts.vue
@@ -27,7 +27,10 @@
           :placeholder="input.placeholder">
         <div>
           <input type="submit" class="ok" :value="prompt.ok">
-          <button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button>
+          <button class="cancel"
+            @click="$store.commit('closeHovers')"
+            :aria-label="$t('buttons.cancel')"
+            :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
         </div>
       </form>
     </template>
diff --git a/assets/src/components/prompts/Rename.vue b/assets/src/components/prompts/Rename.vue
index 2e0bbd92..3951f7d5 100644
--- a/assets/src/components/prompts/Rename.vue
+++ b/assets/src/components/prompts/Rename.vue
@@ -1,11 +1,15 @@
 <template>
   <div class="prompt">
-    <h3>Rename</h3>
-    <p>Insert a new name for <code>{{ oldName() }}</code>:</p>
+    <h3>{{ $t('prompts.rename') }}</h3>
+    <p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
+
     <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
     <div>
-      <button @click="submit" type="submit">Rename</button>
-      <button @click="cancel" class="cancel">Cancel</button>
+      <button @click="submit" type="submit">{{ $t('buttons.rename') }}</button>
+      <button class="cancel"
+        @click="$store.commit('closeHovers')"
+        :aria-label="$t('buttons.cancel')"
+        :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/components/prompts/Success.vue b/assets/src/components/prompts/Success.vue
index 263a846c..b26150af 100644
--- a/assets/src/components/prompts/Success.vue
+++ b/assets/src/components/prompts/Success.vue
@@ -3,7 +3,7 @@
     <i class="material-icons">done</i>
     <h3>{{ $store.state.showMessage }}</h3>
     <div>
-      <button @click="close" autofocus>OK</button>
+      <button @click="close" autofocus>{{ $t('buttons.ok') }}</button>
     </div>
   </div>
 </template>
diff --git a/assets/src/css/dashboard.css b/assets/src/css/dashboard.css
index c873c6bc..003ced21 100644
--- a/assets/src/css/dashboard.css
+++ b/assets/src/css/dashboard.css
@@ -35,7 +35,7 @@
   width: 1em
 }
 
-.dashboard > *:first-child {
+.dashboard > h1:first-of-type {
   margin-top: 0;
 }
 
@@ -48,6 +48,7 @@ form.dashboard > p:last-child {
   margin-bottom: 0;
 }
 
+.dashboard select,
 .dashboard textarea,
 .dashboard input[type="text"],
 .dashboard input[type="password"] {
@@ -60,12 +61,18 @@ form.dashboard > p:last-child {
   width: 100%;
 }
 
+.dashboard #locale,
 .dashboard #username,
 .dashboard #password,
 .dashboard #scope {
   max-width: 18em;
 }
 
+.dashboard #locale {
+  border: 1px solid #dddddd;
+  margin-top: .5em;
+}
+
 .dashboard textarea:focus,
 .dashboard textarea:hover,
 .dashboard input[type="text"]:focus,
@@ -118,3 +125,27 @@ p code {
   font-size: .8em;
   line-height: 1.5;
 }
+
+.dashboard #nav {
+  list-style: none;
+  display: flex;
+  color: rgb(84, 110, 122);
+  font-weight: 500;
+  padding: 0 0 1em;
+  margin: 0 0 1em;
+  font-size: .8em;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.dashboard #nav li {
+  width: 100%;
+}
+
+.dashboard #nav li:last-child {
+  text-align: right
+}
+
+.dashboard #nav i {
+  font-size: 1em;
+  vertical-align: middle;
+}
diff --git a/assets/src/css/login.css b/assets/src/css/login.css
new file mode 100644
index 00000000..91180414
--- /dev/null
+++ b/assets/src/css/login.css
@@ -0,0 +1,68 @@
+#login {
+  background: #fff;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+#login img {
+  width: 4em;
+  height: 4em;
+  margin: 0 auto;
+  display: block;
+}
+
+#login h1 {
+  text-align: center;
+  font-size: 2.5em;
+  margin: .4em 0 .67em;
+}
+
+#login form {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  max-width: 16em;
+  width: 90%;
+}
+
+#login input {
+  width: 100%;
+  width: 100%;
+  margin: .5em 0 0;
+}
+
+#login .wrong {
+  background: #F44336;
+  color: #fff;
+  padding: .5em;
+  text-align: center;
+  animation: .2s opac forwards;
+}
+
+@keyframes opac {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+
+#login input[type="text"],
+#login input[type="password"] {
+  padding: .5em 1em;
+  border: 1px solid #e9e9e9;
+  transition: .2s ease border;
+  color: #333;
+}
+
+#login input[type="text"]:focus,
+#login input[type="password"]:focus,
+#login input[type="text"]:hover,
+#login input[type="password"]:hover {
+  border-color: #9f9f9f;
+}
diff --git a/assets/src/css/styles.css b/assets/src/css/styles.css
index fbd1c131..f9c2f488 100644
--- a/assets/src/css/styles.css
+++ b/assets/src/css/styles.css
@@ -6,6 +6,7 @@
 @import "./listing.css";
 @import "./editor.css";
 @import "./dashboard.css";
+@import "./login.css";
 
 /* * * * * * * * * * * * * * * *
  *            ACTION           *
diff --git a/assets/src/i18n/en.yaml b/assets/src/i18n/en.yaml
new file mode 100644
index 00000000..dd707833
--- /dev/null
+++ b/assets/src/i18n/en.yaml
@@ -0,0 +1,164 @@
+buttons:
+  cancel: Cancel
+  close: Close
+  copy: Copy
+  copyFile: Copy file
+  create: Create
+  delete: Delete
+  download: Download
+  info: Info
+  more: More
+  move: Move
+  moveFile: Move file
+  new: New
+  next: Next
+  ok: OK
+  previous: Previous
+  rename: Rename
+  reportIssue: Report Issue
+  save: Save
+  search: Search
+  select: Select
+  selectMultiple: Select multiple
+  switchView: Swicth view
+  toggleSidebar: Toggle sidebar
+  update: Update
+  upload: Upload
+errors:
+  forbidden: You're not welcome here.
+  internal: Something really went wrong.
+  notFound: This location can't be reached.
+files:
+  folders: Folders
+  files: Files
+  body: Body
+  clear: Clear
+  closePreview: Close preview
+  home: Home
+  lastModified: Last modified
+  loading: Loading...
+  lonely: It feels lonely here...
+  metadata: Metadata
+  multipleSelectionEnabled: Multiple selection enabled
+  name: Name
+  size: Size
+help:
+  click: select file or directory
+  ctrl:
+    click: select multiple files or directories
+    f: opens search
+    s: save a file or download the directory where you are
+  del: delete selected items
+  doubleClick: open a file or directory
+  esc: clear selection and/or close the prompt
+  f1: this information
+  f2: rename file
+  help: Help
+login:
+  password: Password
+  submit: Login
+  username: Username
+  wrongCredentials: Wrong credentials
+prompts:
+  copy: Copy
+  copyMessage: 'Choose the place to copy your files:'
+  currentlyNavigating: 'Currently navigating on:'
+  deleteMessageMultiple: Are you sure you want to delete {count} file(s)?
+  deleteMessageSingle: Are you sure you want to delete this file/folder?
+  deleteTitle: Delete files
+  displayName: 'Display Name:'
+  download: Download files
+  downloadMessage: Choose the format you want to download.
+  error: Something went wrong
+  fileInfo: File information
+  filesSelected: "{count} files selected."
+  lastModified: Last Modified
+  move: Move
+  moveMessage: 'Choose new house for your file(s)/folder(s):'
+  newDir: New directory
+  newDirMessage: Write the name of the new directory.
+  newFile: New file
+  newFileMessage: Write the name of the new file.
+  numberDirs: Number of directories
+  numberFiles: Number of files
+  rename: Rename
+  renameMessage: Insert a new name for
+  show: Show
+  size: Size
+settings:
+  admin: Admin
+  administrator: Administrator
+  allowCommands: Execute commands
+  allowEdit: Edit, rename and delete files or directories.
+  allowNew: Create new files and directories
+  avoidChanges: "(leave blank to avoid changes)"
+  changePassword: Change Password
+  commands: Commands
+  commandsHelp: >
+    Here you can set commands that are executed in the named events. You
+    write one command per line. If the event is related to files, such as before and
+    after saving, the environment variable "file" will be available with the path
+    of the file.
+  commandsUpdated: Commands updated!
+  customStylesheet: Custom Stylesheet
+  examples: Examples
+  globalSettings: Global Settings
+  language: Language
+  newPassword: Your new password
+  newPasswordConfirm: Confirm your new password
+  newUser: New User
+  password: Password
+  passwordUpdated: Password updated!
+  permissions: Permissions
+  permissionsHelp: >
+    You can set the user to be an administrator or choose the permissions
+    individually. If you select "Administrator", all of the other options will be
+    automatically checked. The management of users remains a privilege of an administrator.
+  pluginsUpdated: Plugins settings updated!
+  profileSettings: Profile Settings
+  ruleExample1: >
+    'prevents the access to any dot file (such as .git, .gitignore) in
+    every folder.'
+  ruleExample2: blocks the access to the file named Caddyfile on the root of the scope.
+  rules: Rules
+  rulesHelp1: >
+    'Here you can define a set of allow and disallow rules for this specific
+    user. The blocked files won''t show up in the listings and they won''t be accessible
+    to the user. We support regex and paths relative to the user''s scope.'
+  rulesHelp2: >
+    Each rule goes in one different line and must start with the keyword
+    {0} or {1}. Then you should write {2} if you are using a regular expression and
+    then the expression or the path.
+  scope: Scope
+  settingsUpdated: Settings updated!
+  user: User
+  userCommands: Commands
+  userCommandsHelp:
+    'A space separated list with the available commands for this user.
+    Example:'
+  userCreated: User created!
+  userDeleted: User deleted!
+  userManagement: User Management
+  username: Username
+  users: Users
+  userUpdated: User updated!
+sidebar:
+  help: Help
+  logout: Logout
+  myFiles: My files
+  newFile: New file
+  newFolder: New folder
+  servedWith: Served with
+  settings: Settings
+search:
+  writeToSearch: Write here to search
+  searchOrCommand: Search or execute a command...
+  searchOrSupportedCommand: 'Search or use one of your supported commands:'
+  search: Search...
+  type: Type and press enter to search.
+  pressToSearch: Press enter to search.
+  pressToExecute: Press enter to execute.
+languages:
+  en: English
+  pt: Portuguese
+  zhCN: Chinese (Simplified)
diff --git a/assets/src/i18n/index.js b/assets/src/i18n/index.js
new file mode 100644
index 00000000..24ea77a1
--- /dev/null
+++ b/assets/src/i18n/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import en from './en.yaml'
+import pt from './pt.yaml'
+import zhCN from './zh-cn.yaml'
+
+Vue.use(VueI18n)
+
+const i18n = new VueI18n({
+  locale: 'en',
+  fallbackLocale: 'en',
+  messages: {
+    'en': en,
+    'pt': pt,
+    'zh-cn': zhCN
+  }
+})
+
+export default i18n
diff --git a/assets/src/i18n/pt.yaml b/assets/src/i18n/pt.yaml
new file mode 100644
index 00000000..b0651136
--- /dev/null
+++ b/assets/src/i18n/pt.yaml
@@ -0,0 +1,165 @@
+buttons:
+  cancel: Cancelar
+  close: Fechar
+  copy: Copiar
+  copyFile: Copiar ficheiro
+  create: Criar
+  delete: Eliminar
+  download: Descarregar
+  info: Info
+  more: Mais
+  move: Mover
+  moveFile: Mover ficheiro
+  new: Novo
+  next: Próximo
+  ok: OK
+  previous: Anterior
+  rename: Renomear
+  reportIssue: Reportar Erro
+  save: Guardar
+  search: Pesquisar
+  select: Selecionar
+  selectMultiple: Selecionar múltiplos
+  switchView: Alterar modo de visão
+  toggleSidebar: Alternar barra lateral
+  update: Atualizar
+  upload: Enviar
+errors:
+  forbidden: Tu não és bem-vindo aqui.
+  internal: Algo correu bastante mal.
+  notFound: Não conseguimos chegar a esta localização.
+files:
+  folders: Pastas
+  files: Ficheiros
+  body: Corpo
+  clear: Limpar
+  closePreview: Fechar pré-visualização
+  home: Início
+  lastModified: Última modificação
+  loading: A carregar...
+  lonely: Sinto-me sozinho...
+  metadata: Metadados
+  multipleSelectionEnabled: Seleção múltipla ativada
+  name: Nome
+  size: Tamanho
+help:
+  click: selecionar pasta ou ficheiro
+  ctrl:
+    click: selecionar várias pastas e ficheiros
+    f: pesquisar
+    s: guardar um ficheiro ou descarregar a pasta em que estás a navegar
+  del: eliminar os ficheiros selecionados
+  doubleClick: abrir pasta ou ficheiro
+  esc: limpar seleção e/ou fechar menu
+  f1: esta informação
+  f2: renomear ficheiro
+  help: Ajuda
+login:
+  password: Palavra-passe
+  submit: Login
+  username: Nome de utilizador
+  wrongCredentials: Dados errados
+prompts:
+  copy: Copiar
+  copyMessage: 'Escolhe um lugar para copiar os ficheiros:'
+  currentlyNavigating: 'A navegar em:'
+  deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)?
+  deleteMessageSingle: Deseja eliminar esta pasta/ficheiro?
+  deleteTitle: Eliminar ficheiros
+  displayName: 'Nome:'
+  download: Descarregar ficheiros
+  downloadMessage: Escolha o formato do ficheiro.
+  error: Algo correu mal
+  fileInfo: Informação do ficheiro
+  filesSelected: "{count} ficheiros selecionados."
+  lastModified: Última Modificação
+  move: Mover
+  moveMessage: 'Escolha uma nova casa para os seus ficheiros:'
+  newDir: Nova pasta
+  newDirMessage: Escreva o nome da nova pasta.
+  newFile: Novo ficheiro
+  newFileMessage: Escreva o nome do novo ficheiro.
+  numberDirs: Número de pastas
+  numberFiles: Número de ficheiros
+  rename: Renomear
+  renameMessage: Insira um novo nome para
+  show: Mostrar
+  size: Tamanho
+settings:
+  admin: Admin
+  administrator: Administrador
+  allowCommands: Executar comandos
+  allowEdit: Editar, renomear e eliminar ficheiros ou pastas
+  allowNew: Criar novos ficheiros e pastas
+  avoidChanges: "(deixe em branco para manter)"
+  changePassword: Alterar Password
+  commands: Comandos
+  commandsHelp: >
+    Pode definir um conjunto de comandos a executar em determiandos eventos. Deve
+    escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
+    como antes e depois de guardar, irá existir uma variável de ambiente denominada
+    "file" com o caminho do ficheiro.
+  commandsUpdated: Comandos atualizados!
+  customStylesheet: Estilos Personalizados
+  examples: Exemplos
+  globalSettings: Configurações Globais
+  language: Linguagem
+  newPassword: Nova palavra-passe
+  newPasswordConfirm: Confirme a nova palavra-passe
+  newUser: Novo Utilizador
+  password: Palavra-passe
+  passwordUpdated: Palavra-passe atualizada!
+  permissions: Permissões
+  permissionsHelp: >
+    Pode definir o utilizador como administrador ou escolher as permissões manualmente.
+    Se selecionar a opção "Administrador", todas as outras opções serão automaticamente
+    selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.
+  pluginsUpdated: Plugins atualizados!
+  profileSettings: Configurações do Utilizador
+  ruleExample1: >
+    previne o acesso a qualquer "dotfile" (como .git, .gitignore) em qualquer pasta
+  ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
+  rules: Regras
+  rulesHelp1: >
+    Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso
+    do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não
+    irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
+    dos ficheiros devem ser relativos à base do utilizador.
+  rulesHelp2: >
+    Cada regra deve ser colocada numa linha diferente e deve começar com as
+    palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2},
+    caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
+    ou a expressão regular.
+  scope: Base
+  settingsUpdated: Configurações atualizadas!
+  user: Utilizador
+  userCommands: Comandos
+  userCommandsHelp:
+    'Uma lista, separada com espaços, de comandos disponíveis para este
+    utilizados. Exemplo:'
+  userCreated: Utilizador criado!
+  userDeleted: Utilizador eliminado!
+  userManagement: Gestão de Utilizadores
+  username: Nome de utilizador
+  users: Utilizadores
+  userUpdated: Utilizador atualizado!
+sidebar:
+  help: Ajuda
+  logout: Sair
+  myFiles: Ficheiros
+  newFile: Novo ficheiro
+  newFolder: Nova pasta
+  servedWith: Servido com
+  settings: Configurações
+search:
+  writeToSearch: Escreva aqui para pesquisar
+  searchOrCommand: Pesquise ou execute um comando...
+  searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
+  search: Pesquise...
+  type: Escreva e prima enter para pesquisar.
+  pressToSearch: Prima enter para pesquisar.
+  pressToExecute: Prima enter para executar.
+languages:
+  en: Inglês
+  pt: Português
+  zhCN: Chinês (Simplificado)
diff --git a/assets/src/i18n/zh-cn.yaml b/assets/src/i18n/zh-cn.yaml
new file mode 100644
index 00000000..0385c30a
--- /dev/null
+++ b/assets/src/i18n/zh-cn.yaml
@@ -0,0 +1,151 @@
+buttons:
+  cancel: 取消
+  close: 关闭
+  copy: 复制
+  copyFile: 复制文件
+  create: 创建
+  delete: 删除
+  download: 下载
+  info: 信息
+  more: 更多
+  move: 移动
+  moveFile: 移动文件
+  new: 新
+  next: 下一步
+  ok: 确定
+  previous: 以前
+  rename: 重命名
+  reportIssue: 报告问题
+  save: 保存
+  search: 搜索
+  select: 选择
+  selectMultiple: 选择多个
+  switchView: 切换显示方式
+  toggleSidebar: 切换侧边栏
+  update: 更新
+  upload: 上传
+errors:
+  forbidden: 你被禁止访问.
+  internal: 内部出现麻烦了.
+  notFound: 找不到文件.
+files:
+  folders: 文件夹
+  files: 文件
+  body: Body
+  clear: 清理
+  closePreview: 关闭预览
+  home: 主页
+  lastModified: 最后修改
+  loading: 加载中...
+  lonely: 这里没有任何文件...
+  metadata: 元数据
+  multipleSelectionEnabled: 启用多选模式(现在可以选择多个文件/文件夹)
+  name: 名称
+  size: 大小
+help:
+  click: 选择文件或目录
+  ctrl:
+    click: 选择多个文件或目录
+    f: 打开搜索框
+    s: 保存文件或下载文件夹
+  del: 删除 所选文件/文件夹
+  doubleClick: 打开文件或目录
+  esc: 清除 当前所有选择 或 关闭提示信息
+  f1: 显示 当前帮助信息
+  f2: 重命名 文件/文件夹
+  help: 帮助
+login:
+  password: 密码
+  submit: 登录
+  username: 用户名
+  wrongCredentials: 账号或密码错误
+prompts:
+  copy: 复制
+  copyMessage: '请选择欲复制至的目录:'
+  currentlyNavigating: '目前正在浏览:'
+  deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
+  deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
+  deleteTitle: 删除文件
+  displayName: '名称:'
+  download: 下载文件
+  downloadMessage: 请选择要下载的压缩格式.
+  error: 出了一点问题...
+  fileInfo: 文件信息
+  filesSelected: '选择 {count} 个文件.'
+  lastModified: 最后修改
+  move: 移动
+  moveMessage: '请选择欲移动至的目录:'
+  newDir: 新建目录
+  newDirMessage: 请输入新建目录的名称.
+  newFile: 新建文件
+  newFileMessage: 请输入新建文件的名称.
+  numberDirs: 目录数
+  numberFiles: 文件数
+  rename: 重命名
+  renameMessage: '请输入新名称, 旧名称是:'
+  show: 揭示
+  size: 大小
+settings:
+  admin: 管理员
+  administrator: 管理员
+  allowCommands: 执行命令(Linux 代码)
+  allowEdit: 编辑、重命名或删除文件/目录.
+  allowNew: 创建新文件和目录.
+  avoidChanges: '(留空以避免更改)'
+  changePassword: 更改密码
+  commands: 命令(linux 代码)
+  commandsHelp: >
+    'Here you can set commands that are executed in the named events.
+    每行一条命令. If the event is related to files, such as before and after saving,
+    the environment variable "file" will be available with the path of the file.'
+  commandsUpdated: 命令更新!
+  customStylesheet: 自定义样式表
+  examples: 例子
+  globalSettings: 全局设置
+  newPassword: 您的新密码
+  newPasswordConfirm: 重输一遍新密码
+  newUser: 新建用户
+  password: 密码
+  passwordUpdated: 密码更新!
+  permissions: 权限
+  permissionsHelp: >
+    '您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
+    将自动检查所有其他选项, 并且该用户可以管理其他用户.'
+  pluginsUpdated: 插件设置更新!
+  profileSettings: 配置文件设置
+  ruleExample1: >
+    '阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
+  ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
+  rules: 规则
+  rulesHelp1: >
+    '这里您可以为特定用户制定一组允许或不允许的规则,
+    阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.'
+  rulesHelp2: >
+    每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
+    然后使用表达式或路径, 则需要在第二列单词加入 {2} .
+  scope: 目录范围
+  user: 用户
+  userCommands: 用户命令(Linux 代码)
+  userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:'
+  userCreated: 用户创建!
+  userDeleted: 用户删除!
+  userManagement: 用户管理
+  username: 用户名
+  users: 用户
+  userUpdated: 用户更新!
+sidebar:
+  help: 帮助
+  logout: 注销
+  myFiles: 我的文件
+  newFile: 新建文件
+  newFolder: 新建文件夹
+  servedWith: 服务提供
+  settings: 设置
+search:
+  writeToSearch: 请输入要搜索的内容
+  searchOrCommand: 搜索或者执行命令(Linux 代码)...
+  searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
+  search: 搜索...
+  type: 键入并按 Enter 键(回车)进行搜索.
+  pressToSearch: 按 Enter 键(回车)进行搜索.
+  pressToExecute: 按 Enter 键(回车)执行.
diff --git a/assets/src/main.js b/assets/src/main.js
index 47008899..33c69173 100644
--- a/assets/src/main.js
+++ b/assets/src/main.js
@@ -2,6 +2,7 @@ import Vue from 'vue'
 import App from './App'
 import store from './store'
 import router from './router'
+import i18n from './i18n'
 
 Vue.config.productionTip = true
 
@@ -10,6 +11,7 @@ new Vue({
   el: '#app',
   store,
   router,
+  i18n,
   template: '<App/>',
   components: { App }
 })
diff --git a/assets/src/router/index.js b/assets/src/router/index.js
index 74e24481..e03ea50a 100644
--- a/assets/src/router/index.js
+++ b/assets/src/router/index.js
@@ -1,15 +1,15 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-import Login from '@/components/Login'
-import Main from '@/components/Main'
-import Files from '@/components/Files'
-import Users from '@/components/Users'
-import User from '@/components/User'
-import GlobalSettings from '@/components/GlobalSettings'
-import ProfileSettings from '@/components/ProfileSettings'
-import error403 from '@/components/errors/403'
-import error404 from '@/components/errors/404'
-import error500 from '@/components/errors/500'
+import Login from '@/views/Login'
+import Layout from '@/views/Layout'
+import Files from '@/views/Files'
+import Users from '@/views/Users'
+import User from '@/views/User'
+import GlobalSettings from '@/views/GlobalSettings'
+import ProfileSettings from '@/views/ProfileSettings'
+import Error403 from '@/views/errors/403'
+import Error404 from '@/views/errors/404'
+import Error500 from '@/views/errors/500'
 import auth from '@/utils/auth.js'
 import store from '@/store'
 
@@ -25,24 +25,18 @@ const router = new Router({
       component: Login,
       beforeEnter: function (to, from, next) {
         auth.loggedIn()
-        .then(() => {
-          next({ path: '/files' })
-        })
-        .catch(() => {
-          document.title = 'Login'
-          next()
-        })
-      }
-    },
-    {
-      path: '/',
-      redirect: {
-        path: '/files/'
+          .then(() => {
+            next({ path: '/files' })
+          })
+          .catch(() => {
+            document.title = 'Login'
+            next()
+          })
       }
     },
     {
       path: '/*',
-      component: Main,
+      component: Layout,
       meta: {
         requiresAuth: true
       },
@@ -75,17 +69,17 @@ const router = new Router({
         {
           path: '/403',
           name: 'Forbidden',
-          component: error403
+          component: Error403
         },
         {
           path: '/404',
           name: 'Not Found',
-          component: error404
+          component: Error404
         },
         {
           path: '/500',
           name: 'Internal Server Error',
-          component: error500
+          component: Error500
         },
         {
           path: '/users',
@@ -95,12 +89,6 @@ const router = new Router({
             requiresAdmin: true
           }
         },
-        {
-          path: '/users/',
-          redirect: {
-            path: '/users'
-          }
-        },
         {
           path: '/users/*',
           name: 'User',
diff --git a/assets/src/store/mutations.js b/assets/src/store/mutations.js
index 6797ef2c..481845f2 100644
--- a/assets/src/store/mutations.js
+++ b/assets/src/store/mutations.js
@@ -1,3 +1,5 @@
+import i18n from '@/i18n'
+
 const mutations = {
   closeHovers: state => {
     state.show = null
@@ -22,8 +24,10 @@ const mutations = {
   },
   setLoading: (state, value) => { state.loading = value },
   setReload: (state, value) => { state.reload = value },
-  setUser: (state, value) => (state.user = value),
-  setUserCSS: (state, value) => (state.user.css = value),
+  setUser: (state, value) => {
+    i18n.locale = value.locale
+    state.user = value
+  },
   setJWT: (state, value) => (state.jwt = value),
   multiple: (state, value) => (state.multiple = value),
   addSelected: (state, value) => (state.selected.push(value)),
diff --git a/assets/src/utils/api.js b/assets/src/utils/api.js
index e9b74f48..04fff2cd 100644
--- a/assets/src/utils/api.js
+++ b/assets/src/utils/api.js
@@ -2,7 +2,7 @@ import store from '@/store'
 
 const ssl = (window.location.protocol === 'https:')
 
-function removePrefix (url) {
+export function removePrefix (url) {
   if (url.startsWith('/files')) {
     return url.slice(6)
   }
@@ -10,7 +10,7 @@ function removePrefix (url) {
   return url
 }
 
-function fetch (url) {
+export function fetch (url) {
   url = removePrefix(url)
 
   return new Promise((resolve, reject) => {
@@ -24,10 +24,7 @@ function fetch (url) {
           resolve(JSON.parse(request.responseText))
           break
         default:
-          reject({
-            message: request.responseText,
-            status: request.status
-          })
+          reject(new Error(request.status))
           break
       }
     }
@@ -36,7 +33,7 @@ function fetch (url) {
   })
 }
 
-function rm (url) {
+export function rm (url) {
   url = removePrefix(url)
 
   return new Promise((resolve, reject) => {
@@ -57,7 +54,7 @@ function rm (url) {
   })
 }
 
-function post (url, content = '') {
+export function post (url, content = '') {
   url = removePrefix(url)
 
   return new Promise((resolve, reject) => {
@@ -78,7 +75,7 @@ function post (url, content = '') {
   })
 }
 
-function put (url, content = '') {
+export function put (url, content = '') {
   url = removePrefix(url)
 
   return new Promise((resolve, reject) => {
@@ -132,15 +129,15 @@ function moveCopy (items, copy = false) {
   return Promise.all(promises)
 }
 
-function move (items) {
+export function move (items) {
   return moveCopy(items)
 }
 
-function copy (items) {
+export function copy (items) {
   return moveCopy(items, true)
 }
 
-function checksum (url, algo) {
+export function checksum (url, algo) {
   url = removePrefix(url)
 
   return new Promise((resolve, reject) => {
@@ -160,7 +157,7 @@ function checksum (url, algo) {
   })
 }
 
-function command (url, command, onmessage, onclose) {
+export function command (url, command, onmessage, onclose) {
   let protocol = (ssl ? 'wss:' : 'ws:')
   url = removePrefix(url)
   url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
@@ -171,7 +168,7 @@ function command (url, command, onmessage, onclose) {
   conn.onclose = onclose
 }
 
-function search (url, search, onmessage, onclose) {
+export function search (url, search, onmessage, onclose) {
   let protocol = (ssl ? 'wss:' : 'ws:')
   url = removePrefix(url)
   url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
@@ -182,7 +179,7 @@ function search (url, search, onmessage, onclose) {
   conn.onclose = onclose
 }
 
-function download (format, ...files) {
+export function download (format, ...files) {
   let url = `${store.state.baseURL}/api/download`
 
   if (files.length === 1) {
@@ -206,7 +203,59 @@ function download (format, ...files) {
   window.open(url)
 }
 
-function getUsers () {
+export function getSettings () {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    request.open('GET', `${store.state.baseURL}/api/settings/`, true)
+    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
+
+    request.onload = () => {
+      switch (request.status) {
+        case 200:
+          resolve(JSON.parse(request.responseText))
+          break
+        default:
+          reject(request.responseText)
+          break
+      }
+    }
+    request.onerror = (error) => reject(error)
+    request.send()
+  })
+}
+
+export function updateSettings (param, which) {
+  return new Promise((resolve, reject) => {
+    let data = {
+      what: 'settings',
+      which: which,
+      data: {}
+    }
+
+    data.data[which] = param
+
+    let request = new window.XMLHttpRequest()
+    request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
+    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
+
+    request.onload = () => {
+      switch (request.status) {
+        case 200:
+          resolve()
+          break
+        default:
+          reject(request.responseText)
+          break
+      }
+    }
+    request.onerror = (error) => { reject(error) }
+    request.send(JSON.stringify(data))
+  })
+}
+
+// USERS
+
+export function getUsers () {
   return new Promise((resolve, reject) => {
     let request = new window.XMLHttpRequest()
     request.open('GET', `${store.state.baseURL}/api/users/`, true)
@@ -227,7 +276,7 @@ function getUsers () {
   })
 }
 
-function getUser (id) {
+export function getUser (id) {
   return new Promise((resolve, reject) => {
     let request = new window.XMLHttpRequest()
     request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
@@ -248,7 +297,7 @@ function getUser (id) {
   })
 }
 
-function newUser (user) {
+export function newUser (user) {
   return new Promise((resolve, reject) => {
     let request = new window.XMLHttpRequest()
     request.open('POST', `${store.state.baseURL}/api/users/`, true)
@@ -265,11 +314,15 @@ function newUser (user) {
       }
     }
     request.onerror = (error) => reject(error)
-    request.send(JSON.stringify(user))
+    request.send(JSON.stringify({
+      what: 'user',
+      which: 'new',
+      data: user
+    }))
   })
 }
 
-function updateUser (user) {
+export function updateUser (user, which) {
   return new Promise((resolve, reject) => {
     let request = new window.XMLHttpRequest()
     request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
@@ -286,11 +339,15 @@ function updateUser (user) {
       }
     }
     request.onerror = (error) => reject(error)
-    request.send(JSON.stringify(user))
+    request.send(JSON.stringify({
+      what: 'user',
+      which: (typeof which === 'string') ? which : 'all',
+      data: user
+    }))
   })
 }
 
-function deleteUser (id) {
+export function deleteUser (id) {
   return new Promise((resolve, reject) => {
     let request = new window.XMLHttpRequest()
     request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
@@ -311,133 +368,8 @@ function deleteUser (id) {
   })
 }
 
-function updatePassword (password) {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve()
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send(JSON.stringify({ 'password': password }))
-  })
-}
-
-function updateCSS (css) {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve()
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send(JSON.stringify({ 'css': css }))
-  })
-}
-
-function getCommands () {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('GET', `${store.state.baseURL}/api/commands/`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve(JSON.parse(request.responseText))
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send()
-  })
-}
-
-function updateCommands (commands) {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('PUT', `${store.state.baseURL}/api/commands/`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve()
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send(JSON.stringify(commands))
-  })
-}
-
-function getPlugins () {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('GET', `${store.state.baseURL}/api/plugins/`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve(JSON.parse(request.responseText))
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send()
-  })
-}
-
-function updatePlugins (data) {
-  return new Promise((resolve, reject) => {
-    let request = new window.XMLHttpRequest()
-    request.open('PUT', `${store.state.baseURL}/api/plugins/`, true)
-    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
-
-    request.onload = () => {
-      switch (request.status) {
-        case 200:
-          resolve()
-          break
-        default:
-          reject(request.responseText)
-          break
-      }
-    }
-    request.onerror = (error) => reject(error)
-    request.send(JSON.stringify(data))
-  })
-}
-
 export default {
+  removePrefix,
   delete: rm,
   fetch,
   checksum,
@@ -448,16 +380,13 @@ export default {
   command,
   search,
   download,
-  getUser,
+  // other things
+  getSettings,
+  updateSettings,
+  // User things
   newUser,
-  updateUser,
+  getUser,
   getUsers,
-  updatePassword,
-  updateCSS,
-  getCommands,
-  updateCommands,
-  removePrefix,
-  getPlugins,
-  updatePlugins,
+  updateUser,
   deleteUser
 }
diff --git a/assets/src/utils/auth.js b/assets/src/utils/auth.js
index bc25b343..cde1de92 100644
--- a/assets/src/utils/auth.js
+++ b/assets/src/utils/auth.js
@@ -23,10 +23,10 @@ function loggedIn () {
         parseToken(request.responseText)
         resolve()
       } else {
-        reject()
+        reject(new Error(request.responseText))
       }
     }
-    request.onerror = () => reject()
+    request.onerror = () => reject(new Error('Could not finish the request'))
     request.send()
   })
 }
@@ -45,7 +45,7 @@ function login (user, password) {
         reject(request.responseText)
       }
     }
-    request.onerror = () => reject()
+    request.onerror = () => reject(new Error('Could not finish the request'))
     request.send(JSON.stringify(data))
   })
 }
diff --git a/assets/src/components/Files.vue b/assets/src/views/Files.vue
similarity index 83%
rename from assets/src/components/Files.vue
rename to assets/src/views/Files.vue
index 38924647..5492823c 100644
--- a/assets/src/components/Files.vue
+++ b/assets/src/views/Files.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <div id="breadcrumbs">
-      <router-link to="/files/">
+      <router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
         <i class="material-icons">home</i>
       </router-link>
 
@@ -11,8 +11,8 @@
       </span>
     </div>
     <div v-if="error">
-      <not-found v-if="error === 404"></not-found>
-      <forbidden v-else-if="error === 403"></forbidden>
+      <not-found v-if="error.message === '404'"></not-found>
+      <forbidden v-else-if="error.message === '403'"></forbidden>
       <internal-error v-else></internal-error>
     </div>
     <editor v-else-if="isEditor"></editor>
@@ -20,7 +20,7 @@
     <preview v-else-if="isPreview"></preview>
     <div v-else>
       <h2 class="message">
-        <span>Loading...</span>
+        <span>{{ $t('files.loading') }}</span>
       </h2>
     </div>
   </div>
@@ -30,9 +30,9 @@
 import Forbidden from './errors/403'
 import NotFound from './errors/404'
 import InternalError from './errors/500'
-import Preview from './Preview'
-import Listing from './Listing'
-import Editor from './Editor'
+import Preview from '@/components/files/Preview'
+import Listing from '@/components/files/Listing'
+import Editor from '@/components/files/Editor'
 import api from '@/utils/api'
 import { mapGetters, mapState, mapMutations } from 'vuex'
 
@@ -116,20 +116,11 @@ export default {
   },
   mounted () {
     window.addEventListener('keydown', this.keyEvent)
-    window.addEventListener('scroll', event => {
-      if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
-
-      let top = 112 - window.scrollY
-
-      if (top < 64) {
-        top = 64
-      }
-
-      document.querySelector('#listing.list .item.header').style.top = top + 'px'
-    })
+    window.addEventListener('scroll', this.scroll)
   },
   beforeDestroy () {
     window.removeEventListener('keydown', this.keyEvent)
+    window.removeEventListener('scroll', this.scroll)
   },
   destroyed () {
     this.$store.commit('updateRequest', {})
@@ -152,25 +143,19 @@ export default {
       if (url[0] !== '/') url = '/' + url
 
       api.fetch(url)
-      .then((req) => {
-        if (!url.endsWith('/') && req.url.endsWith('/')) {
-          window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
-        }
+        .then((req) => {
+          if (!url.endsWith('/') && req.url.endsWith('/')) {
+            window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
+          }
 
-        this.$store.commit('updateRequest', req)
-        document.title = req.name
-        this.setLoading(false)
-      })
-      .catch(error => {
-        this.setLoading(false)
-
-        if (typeof error === 'object') {
-          this.error = error.status
-          return
-        }
-
-        this.error = error
-      })
+          this.$store.commit('updateRequest', req)
+          document.title = req.name
+          this.setLoading(false)
+        })
+        .catch(error => {
+          this.setLoading(false)
+          this.error = error
+        })
     },
     keyEvent (event) {
       // Esc!
@@ -220,11 +205,21 @@ export default {
 
           if (this.req.kind !== 'editor') {
             document.getElementById('download-button').click()
-            return
           }
         }
       }
     },
+    scroll (event) {
+      if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
+
+      let top = 112 - window.scrollY
+
+      if (top < 64) {
+        top = 64
+      }
+
+      document.querySelector('#listing.list .item.header').style.top = top + 'px'
+    },
     openSidebar () {
       this.$store.commit('showHover', 'sidebar')
     },
diff --git a/assets/src/components/GlobalSettings.vue b/assets/src/views/GlobalSettings.vue
similarity index 75%
rename from assets/src/components/GlobalSettings.vue
rename to assets/src/views/GlobalSettings.vue
index 0c852b94..0fd04ca0 100644
--- a/assets/src/components/GlobalSettings.vue
+++ b/assets/src/views/GlobalSettings.vue
@@ -1,12 +1,20 @@
 <template>
   <div class="dashboard">
-    <h1>Global Settings</h1>
-
-    <ul>
-      <li><router-link to="/settings/profile">Go to Profile Settings</router-link></li>
-      <li><router-link to="/users">Go to User Management</router-link></li>
+    <ul id="nav">
+      <li>
+        <router-link to="/settings/profile">
+          <i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }}
+        </router-link>
+      </li>
+      <li>
+        <router-link to="/users">
+          {{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
+        </router-link>
+      </li>
     </ul>
 
+    <h1>{{ $t('settings.globalSettings') }}</h1>
+
     <form @submit="savePlugin" v-if="plugins.length > 0">
       <template v-for="plugin in plugins">
         <h2>{{ capitalize(plugin.name) }}</h2>
@@ -23,11 +31,9 @@
     </form>
 
     <form @submit="saveCommands">
-      <h2>Commands</h2>
+      <h2>{{ $t('settings.commands') }}</h2>
 
-      <p class="small">Here you can set commands that are executed in the named events. You write one command
-        per line. If the event is related to files, such as before and after saving, the environment variable
-        <code>file</code> will be available with the path of the file.</p>
+      <p class="small">{{ $t('settings.commandsHelp') }}</p>
 
       <template v-for="command in commands">
         <h3>{{ capitalize(command.name) }}</h3>
@@ -42,7 +48,7 @@
 
 <script>
 import { mapState, mapMutations } from 'vuex'
-import api from '@/utils/api'
+import { getSettings, updateSettings } from '@/utils/api'
 
 export default {
   name: 'settings',
@@ -56,24 +62,20 @@ export default {
     ...mapState([ 'user' ])
   },
   created () {
-    api.getCommands()
-      .then(commands => {
-        for (let key in commands) {
+    getSettings()
+      .then(settings => {
+        for (let key in settings.plugins) {
+          this.plugins.push(this.parsePlugin(key, settings.plugins[key]))
+        }
+
+        for (let key in settings.commands) {
           this.commands.push({
             name: key,
-            value: commands[key].join('\n')
+            value: settings.commands[key].join('\n')
           })
         }
       })
       .catch(error => { this.showError(error) })
-
-    api.getPlugins()
-      .then(plugins => {
-        for (let key in plugins) {
-          this.plugins.push(this.parsePlugin(key, plugins[key]))
-        }
-      })
-      .catch(error => { this.showError(error) })
   },
   methods: {
     ...mapMutations([ 'showSuccess', 'showError' ]),
@@ -102,8 +104,8 @@ export default {
         commands[command.name] = value
       }
 
-      api.updateCommands(commands)
-        .then(() => { this.showSuccess('Commands updated!') })
+      updateSettings(commands, 'commands')
+        .then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
         .catch(error => { this.showError(error) })
     },
     savePlugin (event) {
@@ -129,10 +131,8 @@ export default {
         plugins[plugin.name] = p
       }
 
-      console.log(plugins)
-
-      api.updatePlugins(plugins)
-        .then(() => { this.showSuccess('Plugins settings updated!') })
+      updateSettings(plugins, 'plugins')
+        .then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) })
         .catch(error => { this.showError(error) })
     },
     parsePlugin (name, plugin) {
diff --git a/assets/src/components/Main.vue b/assets/src/views/Layout.vue
similarity index 84%
rename from assets/src/components/Main.vue
rename to assets/src/views/Layout.vue
index b09a5598..dc0af7a6 100644
--- a/assets/src/components/Main.vue
+++ b/assets/src/views/Layout.vue
@@ -10,13 +10,13 @@
 </template>
 
 <script>
-import Search from './Search'
-import Sidebar from './Sidebar'
-import Prompts from './prompts/Prompts'
-import SiteHeader from './Header'
+import Search from '@/components/Search'
+import Sidebar from '@/components/Sidebar'
+import Prompts from '@/components/prompts/Prompts'
+import SiteHeader from '@/components/Header'
 
 export default {
-  name: 'main',
+  name: 'layout',
   components: {
     Search,
     Sidebar,
diff --git a/assets/src/views/Login.vue b/assets/src/views/Login.vue
new file mode 100644
index 00000000..8e64f349
--- /dev/null
+++ b/assets/src/views/Login.vue
@@ -0,0 +1,42 @@
+<template>
+  <div id="login">
+    <form @submit="submit">
+      <img src="../assets/logo.svg" alt="File Manager">
+      <h1>File Manager</h1>
+      <div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
+      <input type="text" v-model="username" :placeholder="$t('login.username')">
+      <input type="password" v-model="password" :placeholder="$t('login.password')">
+      <input type="submit" :value="$t('login.submit')">
+    </form>
+  </div>
+</template>
+
+<script>
+import auth from '@/utils/auth'
+
+export default {
+  name: 'login',
+  data: function () {
+    return {
+      wrong: false,
+      username: '',
+      password: ''
+    }
+  },
+  methods: {
+    submit: function (event) {
+      event.preventDefault()
+      event.stopPropagation()
+
+      let redirect = this.$route.query.redirect
+      if (redirect === '' || redirect === undefined || redirect === null) {
+        redirect = '/files/'
+      }
+
+      auth.login(this.username, this.password)
+        .then(() => { this.$router.push({ path: redirect }) })
+        .catch(() => { this.wrong = true })
+    }
+  }
+}
+</script>
diff --git a/assets/src/views/ProfileSettings.vue b/assets/src/views/ProfileSettings.vue
new file mode 100644
index 00000000..87c34774
--- /dev/null
+++ b/assets/src/views/ProfileSettings.vue
@@ -0,0 +1,103 @@
+<template>
+  <div class="dashboard">
+    <ul id="nav" v-if="user.admin">
+      <li>
+        <router-link to="/settings/global">
+          {{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i>
+        </router-link>
+      </li>
+    </ul>
+
+    <h1>{{ $t('settings.profileSettings') }}</h1>
+
+    <form @submit="updateSettings">
+      <h3>{{ $t('settings.language') }}</h3>
+      <p><languages id="locale" :selected.sync="locale"></languages></p>
+      <h3>{{ $t('settings.customStylesheet') }}</h3>
+      <textarea v-model="css" name="css"></textarea>
+      <p><input type="submit" :value="$t('buttons.update')"></p>
+    </form>
+
+    <form @submit="updatePassword">
+      <h3>{{ $t('settings.changePassword') }}</h3>
+      <p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
+      <p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
+      <p><input type="submit" :value="$t('buttons.update')"></p>
+    </form>
+  </div>
+</template>
+
+<script>
+import { mapState, mapMutations } from 'vuex'
+import { updateUser } from '@/utils/api'
+import Languages from '@/components/Languages'
+
+export default {
+  name: 'settings',
+  components: {
+    Languages
+  },
+  data: function () {
+    return {
+      password: '',
+      passwordConf: '',
+      css: '',
+      locale: ''
+    }
+  },
+  computed: {
+    ...mapState([ 'user' ]),
+    passwordClass () {
+      if (this.password === '' && this.passwordConf === '') {
+        return ''
+      }
+
+      if (this.password === this.passwordConf) {
+        return 'green'
+      }
+
+      return 'red'
+    }
+  },
+  created () {
+    this.css = this.user.css
+    this.locale = this.user.locale
+  },
+  methods: {
+    ...mapMutations([ 'showSuccess' ]),
+    updatePassword (event) {
+      event.preventDefault()
+
+      if (this.password !== this.passwordConf) {
+        return
+      }
+
+      let user = {
+        ID: this.$store.state.user.ID,
+        password: this.password
+      }
+
+      updateUser(user, 'password').then(location => {
+        this.showSuccess(this.$t('settings.passwordUpdated'))
+      }).catch(e => {
+        this.$store.commit('showError', e)
+      })
+    },
+    updateSettings (event) {
+      event.preventDefault()
+
+      let user = {...this.$store.state.user}
+      user.css = this.css
+      user.locale = this.locale
+
+      updateUser(user, 'partial').then(location => {
+        this.$store.commit('setUser', user)
+        this.$emit('css-updated')
+        this.showSuccess(this.$t('settings.settingsUpdated'))
+      }).catch(e => {
+        this.$store.commit('showError', e)
+      })
+    }
+  }
+}
+</script>
diff --git a/assets/src/components/User.vue b/assets/src/views/User.vue
similarity index 64%
rename from assets/src/components/User.vue
rename to assets/src/views/User.vue
index a2f0546d..99b32761 100644
--- a/assets/src/components/User.vue
+++ b/assets/src/views/User.vue
@@ -1,58 +1,56 @@
 <template>
   <div>
     <form @submit="save" class="dashboard">
-      <h1 v-if="id === 0">New User</h1>
-      <h1 v-else>User {{ username }}</h1>
+      <h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
+      <h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
 
-      <p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
-      <p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
-      <p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
+      <p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
+      <p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
+      <p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
+      <p>
+        <label for="locale">{{ $t('settings.language') }}</label>
+        <languages id="locale" :selected.sync="locale"></languages>
+      </p>
 
-      <h2>Permissions</h2>
+      <h2>{{ $t('settings.permissions') }}</h2>
+      <p class="small">{{ $t('settings.permissionsHelp') }}</p>
 
-      <p class="small">You can set the user to be an administrator or choose the permissions individually.
-        If you select "Administrator", all of the other options will be automatically checked.
-        The management of users remains a privilege of an administrator.</p>
-
-      <p><input type="checkbox" v-model="admin"> Administrator</p>
-      <p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
-      <p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
-      <p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p>
+      <p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
+      <p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
+      <p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
+      <p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
       <p v-for="(value, key) in permissions" :key="key">
         <input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
       </p>
 
-      <h3>Commands</h3>
-
-      <p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
-
+      <h3>{{ $t('settings.userCommands') }}</h3>
+      <p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
       <input type="text" v-model.trim="commands">
 
-      <h2>Rules</h2>
+      <h2>{{ $t('settings.rules') }}</h2>
 
-      <p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
-        show up in the listings and they won't be accessible to the user. We support regex and paths relative to
-        the user's scope.</p>
+      <p class="small">{{ $t('settings.rulesHelp1') }}</p>
 
-      <p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
-        Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
+      <i18n path="settings.rulesHelp2" tag="p" class="small">
+        <code>allow</code><code>disallow</code><code>regex</code>
+      </i18n>
 
-      <p class="small"><strong>Examples</strong></p>
+      <p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
 
       <ul class="small">
-        <li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
-        <li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
+        <li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
+        <li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
       </ul>
 
       <textarea v-model.trim="rules"></textarea>
 
-      <h2>Custom Stylesheet</h2>
+      <h2>{{ $t('settings.customStylesheet') }}</h2>
 
       <textarea name="css"></textarea>
 
       <p>
-        <button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
-        <input type="submit" value="Save">
+        <button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
+        <input type="submit" :value="$t('buttons.save')">
       </p>
     </form>
 
@@ -60,8 +58,13 @@
       <h3>Delete User</h3>
       <p>Are you sure you want to delete this user?</p>
       <div>
-        <button @click="deleteUser" autofocus>Delete</button>
-        <button @click="closeHovers" class="cancel">Cancel</button>
+        <button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
+        <button class="cancel"
+          @click="closeHovers"
+          :aria-label="$t('buttons.cancel')"
+          :title="$t('buttons.cancel')">
+          {{ $t('buttons.cancel') }}
+        </button>
       </div>
     </div>
   </div>
@@ -69,10 +72,12 @@
 
 <script>
 import { mapMutations } from 'vuex'
-import api from '@/utils/api'
+import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
+import Languages from '@/components/Languages'
 
 export default {
   name: 'user',
+  components: { Languages },
   data: () => {
     return {
       id: 0,
@@ -85,6 +90,7 @@ export default {
       username: '',
       filesystem: '',
       rules: '',
+      locale: '',
       css: '',
       commands: ''
     }
@@ -92,7 +98,7 @@ export default {
   computed: {
     passwordPlaceholder () {
       if (this.$route.path === '/users/new') return ''
-      return '(leave blank to avoid changes)'
+      return this.$t('settings.avoidChanges')
     }
   },
   created () {
@@ -119,7 +125,7 @@ export default {
         user = 'base'
       }
 
-      api.getUser(user).then(user => {
+      getUser(user).then(user => {
         this.id = user.ID
         this.admin = user.admin
         this.allowCommands = user.allowCommands
@@ -130,6 +136,7 @@ export default {
         this.commands = user.commands.join(' ')
         this.css = user.css
         this.permissions = user.permissions
+        this.locale = user.locale
 
         for (let rule of user.rules) {
           if (rule.allow) {
@@ -173,6 +180,7 @@ export default {
       this.username = ''
       this.filesystem = ''
       this.rules = ''
+      this.locale = ''
       this.css = ''
       this.commands = ''
     },
@@ -182,9 +190,9 @@ export default {
     deleteUser (event) {
       event.preventDefault()
 
-      api.deleteUser(this.id).then(location => {
+      deleteUser(this.id).then(location => {
         this.$router.push({ path: '/users' })
-        this.$store.commit('showSuccess', 'User deleted!')
+        this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
       }).catch(e => {
         this.$store.commit('showError', e)
       })
@@ -194,9 +202,9 @@ export default {
       let user = this.parseForm()
 
       if (this.$route.path === '/users/new') {
-        api.newUser(user).then(location => {
+        newUser(user).then(location => {
           this.$router.push({ path: location })
-          this.$store.commit('showSuccess', 'User created!')
+          this.$store.commit('showSuccess', this.$t('settings.userCreated'))
         }).catch(e => {
           this.$store.commit('showError', e)
         })
@@ -204,8 +212,12 @@ export default {
         return
       }
 
-      api.updateUser(user).then(location => {
-        this.$store.commit('showSuccess', 'User updated!')
+      updateUser(user).then(location => {
+        if (user.ID === this.$store.state.user.ID) {
+          this.$store.commit('setUser', user)
+        }
+
+        this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
       }).catch(e => {
         this.$store.commit('showError', e)
       })
@@ -222,6 +234,7 @@ export default {
         allowEdit: this.allowEdit,
         permissions: this.permissions,
         css: this.css,
+        locale: this.locale,
         commands: this.commands.split(' '),
         rules: []
       }
@@ -269,7 +282,3 @@ export default {
   }
 }
 </script>
-
-<style>
-
-</style>
diff --git a/assets/src/components/Users.vue b/assets/src/views/Users.vue
similarity index 74%
rename from assets/src/components/Users.vue
rename to assets/src/views/Users.vue
index 71ce0c53..81081a2d 100644
--- a/assets/src/components/Users.vue
+++ b/assets/src/views/Users.vue
@@ -1,12 +1,12 @@
 <template>
   <div class="dashboard">
-    <h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
+    <h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
 
     <table>
       <tr>
-        <th>Username</th>
-        <th>Admin</th>
-        <th>Scope</th>
+        <th>{{ $t('settings.username') }}</th>
+        <th>{{ $t('settings.admin') }}</th>
+        <th>{{ $t('settings.scope') }}</th>
         <th></th>
       </tr>
 
diff --git a/assets/src/components/errors/403.vue b/assets/src/views/errors/403.vue
similarity index 78%
rename from assets/src/components/errors/403.vue
rename to assets/src/views/errors/403.vue
index 5c19daf5..47c6c897 100644
--- a/assets/src/components/errors/403.vue
+++ b/assets/src/views/errors/403.vue
@@ -2,7 +2,7 @@
   <div>
     <h2 class="message">
       <i class="material-icons">error</i>
-      <span>You're not welcome here.</span>
+      <span>{{ $t('errors.forbidden') }}</span>
     </h2>
   </div>
 </template>
diff --git a/assets/src/components/errors/404.vue b/assets/src/views/errors/404.vue
similarity index 77%
rename from assets/src/components/errors/404.vue
rename to assets/src/views/errors/404.vue
index d73682e3..61dbe144 100644
--- a/assets/src/components/errors/404.vue
+++ b/assets/src/views/errors/404.vue
@@ -2,7 +2,7 @@
   <div>
     <h2 class="message">
       <i class="material-icons">gps_off</i>
-      <span>This location can't be reached.</span>
+      <span>{{ $t('errors.notFound') }}</span>
     </h2>
   </div>
 </template>
diff --git a/assets/src/components/errors/500.vue b/assets/src/views/errors/500.vue
similarity index 79%
rename from assets/src/components/errors/500.vue
rename to assets/src/views/errors/500.vue
index 681f78a9..0bd86786 100644
--- a/assets/src/components/errors/500.vue
+++ b/assets/src/views/errors/500.vue
@@ -2,7 +2,7 @@
   <div>
     <h2 class="message">
       <i class="material-icons">error_outline</i>
-      <span>Something really went wrong.</span>
+      <span>{{ $t('errors.internal') }}</span>
     </h2>
   </div>
 </template>
diff --git a/auth.go b/auth.go
index 81736ea9..d48f7e92 100644
--- a/auth.go
+++ b/auth.go
@@ -27,7 +27,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
 	}
 
 	// Checks if the user exists.
-	u, ok := c.FM.Users[cred.Username]
+	u, ok := c.Users[cred.Username]
 	if !ok {
 		return http.StatusForbidden, nil
 	}
@@ -78,7 +78,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
 
 	// Creates the token and signs it.
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
-	string, err := token.SignedString(c.FM.key)
+	string, err := token.SignedString(c.key)
 
 	if err != nil {
 		return http.StatusInternalServerError, err
@@ -114,7 +114,7 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
 // User if it is valid.
 func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
 	keyFunc := func(token *jwt.Token) (interface{}, error) {
-		return c.FM.key, nil
+		return c.key, nil
 	}
 	var claims claims
 	token, err := request.ParseFromRequestWithClaims(r,
@@ -127,7 +127,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
 		return false, nil
 	}
 
-	u, ok := c.FM.Users[claims.User.Username]
+	u, ok := c.Users[claims.User.Username]
 	if !ok {
 		return false, nil
 	}
diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go
index 75937beb..c7e8899e 100644
--- a/cmd/filemanager/main.go
+++ b/cmd/filemanager/main.go
@@ -28,6 +28,7 @@ var (
 	commands      string
 	logfile       string
 	plugin        string
+	locale        string
 	port          int
 	allowCommands bool
 	allowEdit     bool
@@ -47,6 +48,7 @@ func init() {
 	flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
 	flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
 	flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
+	flag.StringVar(&locale, "locale", "en", "Default locale for new users")
 	flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable")
 	flag.BoolVarP(&showVer, "version", "v", false, "Show version")
 }
@@ -62,6 +64,7 @@ func setupViper() {
 	viper.SetDefault("AllowEdit", true)
 	viper.SetDefault("AllowNew", true)
 	viper.SetDefault("Plugin", "")
+	viper.SetDefault("Locale", "en")
 
 	viper.BindPFlag("Port", flag.Lookup("port"))
 	viper.BindPFlag("Address", flag.Lookup("address"))
@@ -72,6 +75,7 @@ func setupViper() {
 	viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
 	viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
 	viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
+	viper.BindPFlag("Locale", flag.Lookup("locale"))
 	viper.BindPFlag("Plugin", flag.Lookup("plugin"))
 
 	viper.SetConfigName("filemanager")
@@ -133,6 +137,7 @@ func main() {
 		AllowNew:      viper.GetBool("AllowNew"),
 		Commands:      viper.GetStringSlice("Commands"),
 		Rules:         []*filemanager.Rule{},
+		Locale:        viper.GetString("Locale"),
 		CSS:           "",
 		FileSystem:    fileutils.Dir(viper.GetString("Scope")),
 	})
diff --git a/download.go b/download.go
index 5b6a5130..ce52f7b4 100644
--- a/download.go
+++ b/download.go
@@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 
 	// If the file isn't a directory, serve it using http.ServeFile. We display it
 	// inline if it is requested.
-	if !c.FI.IsDir {
+	if !c.File.IsDir {
 		if r.URL.Query().Get("inline") == "true" {
 			w.Header().Set("Content-Disposition", "inline")
 		} else {
-			w.Header().Set("Content-Disposition", "attachment; filename="+c.FI.Name)
+			w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
 		}
 
-		http.ServeFile(w, r, c.FI.Path)
+		http.ServeFile(w, r, c.File.Path)
 		return 0, nil
 	}
 
@@ -46,10 +46,10 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 
 			// Clean the slashes.
 			name = fileutils.SlashClean(name)
-			files = append(files, filepath.Join(c.FI.Path, name))
+			files = append(files, filepath.Join(c.File.Path, name))
 		}
 	} else {
-		files = append(files, c.FI.Path)
+		files = append(files, c.File.Path)
 	}
 
 	// If the format is true, just set it to "zip".
@@ -93,7 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 	}
 
 	// Defines the file name.
-	name := c.FI.Name
+	name := c.File.Name
 	if name == "." || name == "" {
 		name = "download"
 	}
diff --git a/file.go b/file.go
index 675744f2..db9965ec 100644
--- a/file.go
+++ b/file.go
@@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
 func (i *file) getListing(c *RequestContext, r *http.Request) error {
 	// Gets the directory information using the Virtual File System of
 	// the user configuration.
-	f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
+	f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
 	if err != nil {
 		return err
 	}
diff --git a/filemanager.go b/filemanager.go
index 92c15d3c..b3788c94 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -70,11 +70,15 @@ import (
 )
 
 var (
-	errUserExist     = errors.New("user already exists")
-	errUserNotExist  = errors.New("user does not exist")
-	errEmptyRequest  = errors.New("request body is empty")
-	errEmptyPassword = errors.New("password is empty")
-	plugins          = map[string]Plugin{}
+	errUserExist          = errors.New("user already exists")
+	errUserNotExist       = errors.New("user does not exist")
+	errEmptyRequest       = errors.New("request body is empty")
+	errEmptyPassword      = errors.New("password is empty")
+	errEmptyUsername      = errors.New("username is empty")
+	errEmptyScope         = errors.New("scope is empty")
+	errWrongDataType      = errors.New("wrong data type")
+	errInvalidUpdateField = errors.New("invalid field to update")
+	plugins               = map[string]Plugin{}
 )
 
 // FileManager is a file manager instance. It should be creating using the
@@ -139,6 +143,9 @@ type User struct {
 	// Custom styles for this user.
 	CSS string `json:"css"`
 
+	// Locale is the language of the user.
+	Locale string `json:"locale"`
+
 	// These indicate if the user can perform certain actions.
 	AllowNew      bool            `json:"allowNew"`      // Create files and folders
 	AllowEdit     bool            `json:"allowEdit"`     // Edit/rename files
@@ -208,6 +215,7 @@ var DefaultUser = User{
 	Rules:         []*Rule{},
 	CSS:           "",
 	Admin:         true,
+	Locale:        "en",
 	FileSystem:    fileutils.Dir("."),
 }
 
@@ -428,23 +436,25 @@ func (m *FileManager) registerPermission(name string, value bool) error {
 // Compatible with http.Handler.
 func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	code, err := serveHTTP(&RequestContext{
-		FM:   m,
-		User: nil,
-		FI:   nil,
+		FileManager: m,
+		User:        nil,
+		File:        nil,
 	}, w, r)
 
-	if code != 0 {
+	if code >= 400 {
 		w.WriteHeader(code)
 
-		if err != nil {
-			log.Print(err)
-			w.Write([]byte(err.Error()))
-		} else {
+		if err == nil {
 			txt := http.StatusText(code)
 			log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
 			w.Write([]byte(txt))
 		}
 	}
+
+	if err != nil {
+		log.Print(err)
+		w.Write([]byte(err.Error()))
+	}
 }
 
 // Allowed checks if the user has permission to access a directory/file.
diff --git a/http.go b/http.go
index 55810311..d33c4a8f 100644
--- a/http.go
+++ b/http.go
@@ -10,9 +10,9 @@ import (
 
 // RequestContext contains the needed information to make handlers work.
 type RequestContext struct {
+	*FileManager
 	User *User
-	FM   *FileManager
-	FI   *file
+	File *file
 	// On API handlers, Router is the APi handler we want.
 	Router string
 }
@@ -21,9 +21,9 @@ type RequestContext struct {
 func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	// Checks if the URL contains the baseURL and strips it. Otherwise, it just
 	// returns a 404 error because we're not supposed to be here!
-	p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL)
+	p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
 
-	if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
+	if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
 		return http.StatusNotFound, nil
 	}
 
@@ -34,7 +34,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 	if r.URL.Path == "/sw.js" {
 		return renderFile(
 			w,
-			c.FM.assets.MustString("sw.js"),
+			c.assets.MustString("sw.js"),
 			"application/javascript",
 			c,
 		)
@@ -65,7 +65,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 
 	return renderFile(
 		w,
-		c.FM.assets.MustString("index.html"),
+		c.assets.MustString("index.html"),
 		"text/html",
 		c,
 	)
@@ -74,13 +74,13 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 // staticHandler handles the static assets path.
 func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	if r.URL.Path != "/static/manifest.json" {
-		http.FileServer(c.FM.assets.HTTPBox()).ServeHTTP(w, r)
+		http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
 		return 0, nil
 	}
 
 	return renderFile(
 		w,
-		c.FM.assets.MustString("static/manifest.json"),
+		c.assets.MustString("static/manifest.json"),
 		"application/json",
 		c,
 	)
@@ -107,7 +107,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 		return http.StatusForbidden, nil
 	}
 
-	for p := range c.FM.Plugins {
+	for p := range c.Plugins {
 		code, err := plugins[p].Handler.Before(c, w, r)
 		if code != 0 || err != nil {
 			return code, err
@@ -116,7 +116,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 
 	if c.Router == "checksum" || c.Router == "download" {
 		var err error
-		c.FI, err = getInfo(r.URL, c.FM, c.User)
+		c.File, err = getInfo(r.URL, c.FileManager, c.User)
 		if err != nil {
 			return errorToHTTP(err, false), err
 		}
@@ -138,10 +138,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 		code, err = resourceHandler(c, w, r)
 	case "users":
 		code, err = usersHandler(c, w, r)
-	case "commands":
-		code, err = commandsHandler(c, w, r)
-	case "plugins":
-		code, err = pluginsHandler(c, w, r)
+	case "settings":
+		code, err = settingsHandler(c, w, r)
 	default:
 		code = http.StatusNotFound
 	}
@@ -150,7 +148,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 		return code, err
 	}
 
-	for p := range c.FM.Plugins {
+	for p := range c.Plugins {
 		code, err := plugins[p].Handler.After(c, w, r)
 		if code != 0 || err != nil {
 			return code, err
@@ -164,7 +162,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	query := r.URL.Query().Get("algo")
 
-	val, err := c.FI.Checksum(query)
+	val, err := c.File.Checksum(query)
 	if err == errInvalidOption {
 		return http.StatusBadRequest, err
 	} else if err != nil {
@@ -198,12 +196,12 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
 	w.Header().Set("Content-Type", contentType+"; charset=utf-8")
 
 	var javascript = ""
-	for name := range c.FM.Plugins {
+	for name := range c.Plugins {
 		javascript += plugins[name].JavaScript + "\n"
 	}
 
 	err := tpl.Execute(w, map[string]interface{}{
-		"BaseURL":    c.FM.RootURL(),
+		"BaseURL":    c.RootURL(),
 		"JavaScript": template.JS(javascript),
 	})
 	if err != nil {
diff --git a/package.json b/package.json
index 3ed7d997..4b2dd4df 100644
--- a/package.json
+++ b/package.json
@@ -14,53 +14,57 @@
     "moment": "^2.18.1",
     "normalize.css": "^7.0.0",
     "vue": "^2.3.3",
+    "vue-i18n": "^7.1.0",
     "vue-router": "^2.7.0",
     "vuex": "^2.3.1"
   },
   "devDependencies": {
-    "autoprefixer": "^6.7.2",
+    "autoprefixer": "^7.1.2",
     "babel-core": "^6.22.1",
     "babel-eslint": "^7.1.1",
-    "babel-loader": "^6.2.10",
+    "babel-loader": "^7.1.1",
     "babel-plugin-transform-runtime": "^6.22.0",
     "babel-preset-env": "^1.3.2",
     "babel-preset-stage-2": "^6.22.0",
     "babel-register": "^6.22.0",
-    "chalk": "^1.1.3",
+    "chalk": "^2.0.1",
     "connect-history-api-fallback": "^1.3.0",
     "copy-webpack-plugin": "^4.0.1",
     "css-loader": "^0.28.0",
-    "eslint": "^3.19.0",
-    "eslint-config-standard": "^6.2.1",
-    "eslint-friendly-formatter": "^2.0.7",
+    "eslint": "^4.3.0",
+    "eslint-config-standard": "^10.2.1",
+    "eslint-friendly-formatter": "^3.0.0",
     "eslint-loader": "^1.7.1",
-    "eslint-plugin-html": "^2.0.0",
+    "eslint-plugin-html": "^3.1.1",
+    "eslint-plugin-import": "^2.7.0",
+    "eslint-plugin-node": "^5.1.1",
     "eslint-plugin-promise": "^3.4.0",
-    "eslint-plugin-standard": "^2.0.1",
+    "eslint-plugin-standard": "^3.0.1",
     "eventsource-polyfill": "^0.9.6",
     "express": "^4.14.1",
-    "extract-text-webpack-plugin": "^2.0.0",
+    "extract-text-webpack-plugin": "^3.0.0",
     "file-loader": "^0.11.1",
     "friendly-errors-webpack-plugin": "^1.1.3",
     "html-webpack-plugin": "^2.28.0",
     "http-proxy-middleware": "^0.17.3",
-    "opn": "^4.0.2",
-    "optimize-css-assets-webpack-plugin": "^1.3.0",
+    "opn": "^5.1.0",
+    "optimize-css-assets-webpack-plugin": "^3.0.0",
     "ora": "^1.2.0",
     "rimraf": "^2.6.0",
     "semver": "^5.3.0",
     "shelljs": "^0.7.6",
-    "sw-precache-webpack-plugin": "^0.9.1",
+    "sw-precache-webpack-plugin": "^0.11.4",
     "uglify-js": "^3.0.23",
     "url-loader": "^0.5.8",
-    "vue-loader": "^12.1.0",
+    "vue-loader": "^13.0.2",
     "vue-style-loader": "^3.0.1",
     "vue-template-compiler": "^2.3.3",
-    "webpack": "^2.6.1",
+    "webpack": "^3.4.1",
     "webpack-bundle-analyzer": "^2.2.1",
     "webpack-dev-middleware": "^1.10.0",
     "webpack-hot-middleware": "^2.18.0",
-    "webpack-merge": "^4.1.0"
+    "webpack-merge": "^4.1.0",
+    "yml-loader": "^2.1.0"
   },
   "engines": {
     "node": ">= 4.0.0",
diff --git a/plugins/hugo.go b/plugins/hugo.go
index 2a70a051..a19c4e41 100644
--- a/plugins/hugo.go
+++ b/plugins/hugo.go
@@ -117,7 +117,7 @@ func (h Hugo) undraft(file string) error {
 type hugo struct{}
 
 func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	o := c.FM.Plugins["hugo"].(*Hugo)
+	o := c.Plugins["hugo"].(*Hugo)
 
 	// If we are using the 'magic url' for the settings, we should redirect the
 	// request for the acutual path.
@@ -189,7 +189,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht
 		filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
 
 		// Before save command handler.
-		if err := c.FM.Runner("before_publish", filename); err != nil {
+		if err := c.Runner("before_publish", filename); err != nil {
 			return http.StatusInternalServerError, err
 		}
 
@@ -205,7 +205,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht
 		o.run(false)
 
 		// Executed the before publish command.
-		if err := c.FM.Runner("before_publish", filename); err != nil {
+		if err := c.Runner("before_publish", filename); err != nil {
 			return http.StatusInternalServerError, err
 		}
 
diff --git a/resource.go b/resource.go
index 1e1766d3..6eff1db6 100644
--- a/resource.go
+++ b/resource.go
@@ -34,7 +34,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 	case http.MethodPut:
 		// Before save command handler.
 		path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
-		if err := c.FM.Runner("before_save", path); err != nil {
+		if err := c.Runner("before_save", path); err != nil {
 			return http.StatusInternalServerError, err
 		}
 
@@ -44,7 +44,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 		}
 
 		// After save command handler.
-		if err := c.FM.Runner("after_save", path); err != nil {
+		if err := c.Runner("after_save", path); err != nil {
 			return http.StatusInternalServerError, err
 		}
 
@@ -60,7 +60,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 
 func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	// Gets the information of the directory/file.
-	f, err := getInfo(r.URL, c.FM, c.User)
+	f, err := getInfo(r.URL, c.FileManager, c.User)
 	if err != nil {
 		return errorToHTTP(err, false), err
 	}
@@ -73,7 +73,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
 
 	// If it is a dir, go and serve the listing.
 	if f.IsDir {
-		c.FI = f
+		c.File = f
 		return listingHandler(c, w, r)
 	}
 
@@ -101,7 +101,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
 }
 
 func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	f := c.FI
+	f := c.File
 	f.Kind = "listing"
 
 	// Tries to get the listing data.
@@ -112,7 +112,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
 	listing := f.listing
 
 	// Defines the cookie scope.
-	cookieScope := c.FM.RootURL()
+	cookieScope := c.RootURL()
 	if cookieScope == "" {
 		cookieScope = "/"
 	}
diff --git a/rice-box.go.REMOVED.git-id b/rice-box.go.REMOVED.git-id
index 18bc6310..70ad3bed 100644
--- a/rice-box.go.REMOVED.git-id
+++ b/rice-box.go.REMOVED.git-id
@@ -1 +1 @@
-49dd472ced00d5e02963554cd0b84393f8c08d75
\ No newline at end of file
+b5a8f3badeeb5ea5e285f23298ddef20ce247376
\ No newline at end of file
diff --git a/settings.go b/settings.go
index e5e5c317..f0b771f5 100644
--- a/settings.go
+++ b/settings.go
@@ -2,66 +2,18 @@ package filemanager
 
 import (
 	"encoding/json"
-	"errors"
 	"net/http"
 	"reflect"
 
 	"github.com/mitchellh/mapstructure"
 )
 
-func commandsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	switch r.Method {
-	case http.MethodGet:
-		return commandsGetHandler(c, w, r)
-	case http.MethodPut:
-		return commandsPutHandler(c, w, r)
-	}
-
-	return http.StatusMethodNotAllowed, nil
-}
-
-func commandsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	if !c.User.Admin {
-		return http.StatusForbidden, nil
-	}
-
-	return renderJSON(w, c.FM.Commands)
-}
-
-func commandsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	if !c.User.Admin {
-		return http.StatusForbidden, nil
-	}
-
-	if r.Body == nil {
-		return http.StatusBadGateway, errors.New("Empty request body")
-	}
-
-	var commands map[string][]string
-
-	// Parses the user and checks for error.
-	err := json.NewDecoder(r.Body).Decode(&commands)
-	if err != nil {
-		return http.StatusBadRequest, errors.New("Invalid JSON")
-	}
-
-	if err := c.FM.db.Set("config", "commands", commands); err != nil {
-		return http.StatusInternalServerError, err
-	}
-
-	c.FM.Commands = commands
-	return http.StatusOK, nil
-}
-
-func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	switch r.Method {
-	case http.MethodGet:
-		return pluginsGetHandler(c, w, r)
-	case http.MethodPut:
-		return pluginsPutHandler(c, w, r)
-	}
-
-	return http.StatusMethodNotAllowed, nil
+type modifySettingsRequest struct {
+	*modifyRequest
+	Data struct {
+		Commands map[string][]string               `json:"commands"`
+		Plugins  map[string]map[string]interface{} `json:"plugins"`
+	} `json:"data"`
 }
 
 type pluginOption struct {
@@ -70,19 +22,63 @@ type pluginOption struct {
 	Value    interface{} `json:"value"`
 }
 
-func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
+	// Checks if the request body is empty.
+	if r.Body == nil {
+		return nil, errEmptyRequest
+	}
+
+	// Parses the request body and checks if it's well formed.
+	mod := &modifySettingsRequest{}
+	err := json.NewDecoder(r.Body).Decode(mod)
+	if err != nil {
+		return nil, err
+	}
+
+	// Checks if the request type is right.
+	if mod.What != "settings" {
+		return nil, errWrongDataType
+	}
+
+	return mod, nil
+}
+
+func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+	if r.URL.Path != "" && r.URL.Path != "/" {
+		return http.StatusNotFound, nil
+	}
+
+	switch r.Method {
+	case http.MethodGet:
+		return settingsGetHandler(c, w, r)
+	case http.MethodPut:
+		return settingsPutHandler(c, w, r)
+	}
+
+	return http.StatusMethodNotAllowed, nil
+}
+
+type settingsGetRequest struct {
+	Commands map[string][]string       `json:"commands"`
+	Plugins  map[string][]pluginOption `json:"plugins"`
+}
+
+func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	if !c.User.Admin {
 		return http.StatusForbidden, nil
 	}
 
-	plugins := map[string][]pluginOption{}
+	result := &settingsGetRequest{
+		Commands: c.Commands,
+		Plugins:  map[string][]pluginOption{},
+	}
 
-	for name, p := range c.FM.Plugins {
-		plugins[name] = []pluginOption{}
+	for name, p := range c.Plugins {
+		result.Plugins[name] = []pluginOption{}
 
 		t := reflect.TypeOf(p).Elem()
 		for i := 0; i < t.NumField(); i++ {
-			plugins[name] = append(plugins[name], pluginOption{
+			result.Plugins[name] = append(result.Plugins[name], pluginOption{
 				Variable: t.Field(i).Name,
 				Name:     t.Field(i).Tag.Get("name"),
 				Value:    reflect.ValueOf(p).Elem().FieldByName(t.Field(i).Name).Interface(),
@@ -90,37 +86,44 @@ func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request
 		}
 	}
 
-	return renderJSON(w, plugins)
+	return renderJSON(w, result)
 }
 
-func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	if !c.User.Admin {
 		return http.StatusForbidden, nil
 	}
 
-	if r.Body == nil {
-		return http.StatusBadGateway, errors.New("Empty request body")
-	}
-
-	var raw map[string]map[string]interface{}
-
-	// Parses the user and checks for error.
-	err := json.NewDecoder(r.Body).Decode(&raw)
+	mod, err := parsePutSettingsRequest(r)
 	if err != nil {
 		return http.StatusBadRequest, err
 	}
-
-	for name, plugin := range raw {
-		err = mapstructure.Decode(plugin, c.FM.Plugins[name])
-		if err != nil {
+	// Update the commands.
+	if mod.Which == "commands" {
+		if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
 			return http.StatusInternalServerError, err
 		}
 
-		err = c.FM.db.Set("plugins", name, c.FM.Plugins[name])
-		if err != nil {
-			return http.StatusInternalServerError, err
-		}
+		c.Commands = mod.Data.Commands
+		return http.StatusOK, nil
 	}
 
-	return http.StatusOK, nil
+	// Update the plugins.
+	if mod.Which == "plugins" {
+		for name, plugin := range mod.Data.Plugins {
+			err = mapstructure.Decode(plugin, c.Plugins[name])
+			if err != nil {
+				return http.StatusInternalServerError, err
+			}
+
+			err = c.db.Set("plugins", name, c.Plugins[name])
+			if err != nil {
+				return http.StatusInternalServerError, err
+			}
+		}
+
+		return http.StatusOK, nil
+	}
+
+	return http.StatusMethodNotAllowed, nil
 }
diff --git a/users.go b/users.go
index 6eab4e03..1171d749 100644
--- a/users.go
+++ b/users.go
@@ -11,20 +11,22 @@ import (
 	"github.com/asdine/storm"
 )
 
+type modifyRequest struct {
+	What  string `json:"what"`  // Answer to: what data type?
+	Which string `json:"which"` // Answer to: which field?
+}
+
+type modifyUserRequest struct {
+	*modifyRequest
+	Data *User `json:"data"`
+}
+
 // usersHandler is the entry point of the users API. It's just a router
 // to send the request to its
 func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	if r.URL.Path == "/change-password" {
-		return usersUpdatePassword(c, w, r)
-	}
-
-	if r.URL.Path == "/change-css" {
-		return usersUpdateCSS(c, w, r)
-	}
-
-	// If the user is admin and the HTTP Method is not
-	// PUT, then we return forbidden.
-	if !c.User.Admin {
+	// If the user isn't admin and isn't making a PUT
+	// request, then return forbidden.
+	if !c.User.Admin && r.Method != http.MethodPut {
 		return http.StatusForbidden, nil
 	}
 
@@ -61,32 +63,38 @@ func getUserID(r *http.Request) (int, error) {
 // getUser returns the user which is present in the request
 // body. If the body is empty or the JSON is invalid, it
 // returns an error.
-func getUser(r *http.Request) (*User, error) {
+func getUser(r *http.Request) (*User, string, error) {
+	// Checks if the request body is empty.
 	if r.Body == nil {
-		return nil, errEmptyRequest
+		return nil, "", errEmptyRequest
 	}
 
-	u := &User{}
-
-	err := json.NewDecoder(r.Body).Decode(u)
+	// Parses the request body and checks if it's well formed.
+	mod := &modifyUserRequest{}
+	err := json.NewDecoder(r.Body).Decode(mod)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
-	return u, nil
+	// Checks if the request type is right.
+	if mod.What != "user" {
+		return nil, "", errWrongDataType
+	}
+
+	return mod.Data, mod.Which, nil
 }
 
 func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	// Request for the default user data.
 	if r.URL.Path == "/base" {
-		return renderJSON(w, c.FM.DefaultUser)
+		return renderJSON(w, c.DefaultUser)
 	}
 
 	// Request for the listing of users.
 	if r.URL.Path == "/" {
 		users := []User{}
 
-		for _, user := range c.FM.Users {
+		for _, user := range c.Users {
 			// Copies the user info and removes its
 			// password so it won't be sent to the
 			// front-end.
@@ -108,7 +116,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 	}
 
 	// Searches for the user and prints the one who matches.
-	for _, user := range c.FM.Users {
+	for _, user := range c.Users {
 		if user.ID != id {
 			continue
 		}
@@ -127,11 +135,26 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 		return http.StatusMethodNotAllowed, nil
 	}
 
-	u, err := getUser(r)
+	u, _, err := getUser(r)
 	if err != nil {
 		return http.StatusBadRequest, err
 	}
 
+	// Checks if username isn't empty.
+	if u.Username == "" {
+		return http.StatusBadRequest, errEmptyUsername
+	}
+
+	// Checks if filesystem isn't empty.
+	if u.FileSystem == "" {
+		return http.StatusBadRequest, errEmptyScope
+	}
+
+	// Checks if password isn't empty.
+	if u.Password == "" {
+		return http.StatusBadRequest, errEmptyPassword
+	}
+
 	// The username, password and scope cannot be empty.
 	if u.Username == "" || u.Password == "" || u.FileSystem == "" {
 		return http.StatusBadRequest, errors.New("username, password or scope is empty")
@@ -161,7 +184,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 	u.Password = pw
 
 	// Saves the user to the database.
-	err = c.FM.db.Save(u)
+	err = c.db.Save(u)
 	if err == storm.ErrAlreadyExists {
 		return http.StatusConflict, errUserExist
 	}
@@ -171,7 +194,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 	}
 
 	// Saves the user to the memory.
-	c.FM.Users[u.Username] = u
+	c.Users[u.Username] = u
 
 	// Set the Location header and return.
 	w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
@@ -190,7 +213,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
 	}
 
 	// Deletes the user from the database.
-	err = c.FM.db.DeleteStruct(&User{ID: id})
+	err = c.db.DeleteStruct(&User{ID: id})
 	if err == storm.ErrNotFound {
 		return http.StatusNotFound, errUserNotExist
 	}
@@ -200,9 +223,9 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
 	}
 
 	// Delete the user from the in-memory users map.
-	for _, user := range c.FM.Users {
+	for _, user := range c.Users {
 		if user.ID == id {
-			delete(c.FM.Users, user.Username)
+			delete(c.Users, user.Username)
 			break
 		}
 	}
@@ -210,72 +233,79 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
 	return http.StatusOK, nil
 }
 
-func usersUpdatePassword(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	if r.Method != http.MethodPut {
-		return http.StatusMethodNotAllowed, nil
-	}
-
-	u, err := getUser(r)
-	if err != nil {
-		return http.StatusBadRequest, err
-	}
-
-	if u.Password == "" {
-		return http.StatusBadRequest, errEmptyPassword
-	}
-
-	pw, err := hashPassword(u.Password)
-	if err != nil {
-		return http.StatusInternalServerError, err
-	}
-
-	c.User.Password = pw
-	err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
-	if err != nil {
-		return http.StatusInternalServerError, err
-	}
-
-	return http.StatusOK, nil
-}
-
-func usersUpdateCSS(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
-	if r.Method != http.MethodPut {
-		return http.StatusMethodNotAllowed, nil
-	}
-
-	u, err := getUser(r)
-	if err != nil {
-		return http.StatusBadRequest, err
-	}
-
-	c.User.CSS = u.CSS
-	err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
-	if err != nil {
-		return http.StatusInternalServerError, err
-	}
-
-	return http.StatusOK, nil
-}
-
 func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	// New users should be created on /api/users.
 	if r.URL.Path == "/" {
 		return http.StatusMethodNotAllowed, nil
 	}
 
+	// Gets the user ID from the URL and checks if it's valid.
 	id, err := getUserID(r)
 	if err != nil {
 		return http.StatusInternalServerError, err
 	}
 
-	u, err := getUser(r)
+	// Checks if the user has permission to access this page.
+	if !c.User.Admin && id != c.User.ID {
+		return http.StatusForbidden, nil
+	}
+
+	// Gets the user from the request body.
+	u, which, err := getUser(r)
 	if err != nil {
 		return http.StatusBadRequest, err
 	}
 
-	// The username and the filesystem cannot be empty.
-	if u.Username == "" || u.FileSystem == "" {
-		return http.StatusBadRequest, errors.New("Username, password or scope are empty")
+	// Updates the CSS and locale.
+	if which == "partial" {
+		c.User.CSS = u.CSS
+		c.User.Locale = u.Locale
+		err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
+		if err != nil {
+			return http.StatusInternalServerError, err
+		}
+
+		err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
+		if err != nil {
+			return http.StatusInternalServerError, err
+		}
+
+		return http.StatusOK, nil
+	}
+
+	// Updates the Password.
+	if which == "password" {
+		if u.Password == "" {
+			return http.StatusBadRequest, errEmptyPassword
+		}
+
+		pw, err := hashPassword(u.Password)
+		if err != nil {
+			return http.StatusInternalServerError, err
+		}
+
+		c.User.Password = pw
+		err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
+		if err != nil {
+			return http.StatusInternalServerError, err
+		}
+
+		return http.StatusOK, nil
+	}
+
+	// If can only be all.
+	if which != "all" {
+		return http.StatusBadRequest, errInvalidUpdateField
+	}
+
+	// Checks if username isn't empty.
+	if u.Username == "" {
+		return http.StatusBadRequest, errEmptyUsername
+	}
+
+	// Checks if filesystem isn't empty.
+	if u.FileSystem == "" {
+		return http.StatusBadRequest, errEmptyScope
 	}
 
 	// Initialize rules if they're not initialized.
@@ -288,48 +318,50 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 		u.Commands = []string{}
 	}
 
-	var ouser *User
-	for _, user := range c.FM.Users {
+	// Gets the current saved user from the in-memory map.
+	var suser *User
+	for _, user := range c.Users {
 		if user.ID == id {
-			ouser = user
+			suser = user
 			break
 		}
 	}
-
-	if ouser == nil {
+	if suser == nil {
 		return http.StatusNotFound, nil
 	}
 
 	u.ID = id
 
-	if u.Password == "" {
-		u.Password = ouser.Password
-	} else {
+	// Changes the password if the request wants it.
+	if u.Password != "" {
 		pw, err := hashPassword(u.Password)
 		if err != nil {
 			return http.StatusInternalServerError, err
 		}
 
 		u.Password = pw
+	} else {
+		u.Password = suser.Password
 	}
 
+	// Default permissions if current are nil.
 	if u.Permissions == nil {
-		u.Permissions = c.FM.DefaultUser.Permissions
+		u.Permissions = c.DefaultUser.Permissions
 	}
 
 	// Updates the whole User struct because we always are supposed
 	// to send a new entire object.
-	err = c.FM.db.Save(u)
+	err = c.db.Save(u)
 	if err != nil {
 		return http.StatusInternalServerError, err
 	}
 
 	// If the user changed the username, delete the old user
 	// from the in-memory user map.
-	if ouser.Username != u.Username {
-		delete(c.FM.Users, ouser.Username)
+	if suser.Username != u.Username {
+		delete(c.Users, suser.Username)
 	}
 
-	c.FM.Users[u.Username] = u
+	c.Users[u.Username] = u
 	return http.StatusOK, nil
 }