From 265c3cded6e5b249ab640c15dd10961318162056 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Sun, 1 Jul 2018 21:50:50 +0200
Subject: [PATCH] Localized some strings

---
 front/src/components/About.vue                |  9 ++-
 front/src/components/Home.vue                 | 11 ++--
 front/src/components/PageNotFound.vue         |  9 ++-
 front/src/components/Sidebar.vue              | 12 +++-
 front/src/components/audio/PlayButton.vue     |  7 ++-
 front/src/components/audio/Player.vue         | 56 +++++++++++++++----
 front/src/components/audio/Search.vue         |  9 ++-
 front/src/components/audio/SearchBar.vue      | 18 ++++--
 front/src/components/auth/Login.vue           | 14 ++++-
 front/src/components/auth/Logout.vue          | 10 +++-
 front/src/components/auth/Profile.vue         |  9 ++-
 front/src/components/auth/Settings.vue        |  9 ++-
 front/src/components/auth/Signup.vue          | 38 +++++++++----
 front/src/components/common/ActionTable.vue   |  2 +-
 front/src/components/favorites/List.vue       |  9 ++-
 .../federation/LibraryFollowTable.vue         |  9 ++-
 .../src/components/federation/LibraryForm.vue |  7 ++-
 .../federation/LibraryTrackTable.vue          |  7 ++-
 front/src/components/forms/PasswordInput.vue  |  7 ++-
 front/src/components/instance/Stats.vue       |  2 +-
 front/src/components/library/Album.vue        |  7 ++-
 front/src/components/library/Artist.vue       |  7 ++-
 front/src/components/library/Artists.vue      | 18 ++++--
 front/src/components/library/Home.vue         |  9 ++-
 front/src/components/library/Radios.vue       | 14 ++++-
 front/src/components/library/Track.vue        |  7 ++-
 .../library/import/ArtistImport.vue           |  7 ++-
 .../components/library/import/BatchDetail.vue | 21 +++++--
 .../components/library/import/BatchList.vue   | 14 ++++-
 front/src/components/library/import/Main.vue  |  9 ++-
 .../src/components/library/radios/Builder.vue | 12 +++-
 .../components/manage/library/FilesTable.vue  | 11 +++-
 .../manage/library/RequestsTable.vue          | 13 +++--
 .../manage/users/InvitationForm.vue           | 11 +++-
 .../manage/users/InvitationsTable.vue         | 11 +++-
 .../components/manage/users/UsersTable.vue    |  7 ++-
 front/src/components/metadata/ArtistCard.vue  |  9 ++-
 front/src/components/metadata/ReleaseCard.vue | 11 +++-
 front/src/components/metadata/Search.vue      |  7 ++-
 front/src/components/playlists/Card.vue       |  2 +-
 front/src/components/playlists/Editor.vue     |  9 ++-
 front/src/components/playlists/Form.vue       |  7 ++-
 .../components/playlists/PlaylistModal.vue    |  7 ++-
 .../playlists/TrackPlaylistIcon.vue           |  9 ++-
 front/src/components/requests/Form.vue        | 20 +++++--
 front/src/views/admin/Settings.vue            |  7 ++-
 front/src/views/admin/library/Base.vue        | 17 +++++-
 front/src/views/admin/library/FilesList.vue   |  9 ++-
 .../src/views/admin/library/RequestsList.vue  |  9 ++-
 front/src/views/admin/users/Base.vue          | 12 +++-
 .../src/views/admin/users/InvitationsList.vue |  9 ++-
 front/src/views/admin/users/UsersDetail.vue   |  7 ++-
 front/src/views/admin/users/UsersList.vue     |  9 ++-
 front/src/views/auth/EmailConfirm.vue         |  9 ++-
 front/src/views/auth/PasswordReset.vue        | 14 ++++-
 front/src/views/auth/PasswordResetConfirm.vue |  9 ++-
 front/src/views/federation/Base.vue           | 14 ++++-
 front/src/views/federation/LibraryDetail.vue  | 22 ++++++--
 .../views/federation/LibraryFollowersList.vue |  9 ++-
 front/src/views/federation/LibraryList.vue    | 14 ++++-
 .../src/views/federation/LibraryTrackList.vue |  9 ++-
 front/src/views/instance/Timeline.vue         |  9 ++-
 front/src/views/playlists/Detail.vue          | 11 +++-
 front/src/views/playlists/List.vue            | 14 ++++-
 front/src/views/radios/Detail.vue             | 17 ++++--
 65 files changed, 603 insertions(+), 140 deletions(-)

diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index a4597b29..c9e1e23c 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'About This Instance'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
@@ -49,7 +49,12 @@ export default {
   computed: {
     ...mapState({
       instance: state => state.instance.settings.instance
-    })
+    }),
+    labels () {
+      return {
+        title: this.$gettext('About this instance')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue
index 2f6196ea..648b1afc 100644
--- a/front/src/components/Home.vue
+++ b/front/src/components/Home.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Welcome'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
@@ -146,9 +146,12 @@
 <script>
 
 export default {
-  name: 'home',
-  data () {
-    return {}
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Welcome')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/components/PageNotFound.vue b/front/src/components/PageNotFound.vue
index 46b620c0..115bc5d0 100644
--- a/front/src/components/PageNotFound.vue
+++ b/front/src/components/PageNotFound.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Page Not Found'">
+  <div class="main pusher" :v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
@@ -26,6 +26,13 @@ export default {
     return {
       path: window.location.href
     }
+  },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Page Not Found')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 8cdf4254..beced0b4 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -70,7 +70,7 @@
               <i class="book icon"></i><translate>Library</translate>
               <div
                 :class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']"
-                :title="$gettext('Pending import requests')">
+                :title="labels.pendingRequests">
                 {{ $store.state.ui.notifications.importRequests }}</div>
 
             </router-link>
@@ -87,7 +87,7 @@
               <i class="sitemap icon"></i><translate>Federation</translate>
               <div
                 :class="['ui', {'teal': $store.state.ui.notifications.federation > 0}, 'label']"
-                :title="$gettext('Pending follow requests')">
+                :title="labels.pendingFollows">
                 {{ $store.state.ui.notifications.federation }}</div>
             </router-link>
             <router-link
@@ -211,6 +211,14 @@ export default {
       queue: state => state.queue,
       url: state => state.route.path
     }),
+    labels () {
+      let pendingRequests = this.$gettext('Pending import requests')
+      let pendingFollows = this.$gettext('Pending follow requests')
+      return {
+        pendingRequests,
+        pendingFollows
+      }
+    },
     showAdmin () {
       let adminPermissions = [
         this.$store.state.auth.availablePermissions['federation'],
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 497229d4..6c5ebbc2 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -1,7 +1,7 @@
 <template>
   <div :title="title" :class="['ui', {'tiny': discrete}, 'buttons']">
     <button
-      :title="$gettext('Add to current queue')"
+      :title="labels.addToQueue"
       @click="addNext(true)"
       :disabled="!playable"
       :class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}, 'button']">
@@ -42,6 +42,11 @@ export default {
     jQuery(this.$el).find('.ui.dropdown').dropdown()
   },
   computed: {
+    labels () {
+      return {
+        addToQueue: this.$gettext('Add to current queue')
+      }
+    },
     title () {
       if (this.playable) {
         return this.$gettext('Play immediatly')
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 37457df7..ef9356c9 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -57,44 +57,50 @@
 
       <div class="two wide column controls ui grid">
         <div
-          :title="$gettext('Previous track')"
+          :title="labels.previousTrack"
           class="two wide column control"
           :disabled="emptyQueue">
             <i @click="previous" :class="['ui', 'backward', {'disabled': emptyQueue}, 'big', 'icon']"></i>
         </div>
         <div
           v-if="!playing"
-          :title="$gettext('Play track')"
+          :title="labels.play"
           class="two wide column control">
             <i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
           v-else
-          :title="$gettext('Pause track')"
+          :title="labels.pause"
           class="two wide column control">
             <i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
-          :title="$gettext('Next track')"
+          :title="labels.next"
           class="two wide column control"
           :disabled="!hasNext">
             <i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
         </div>
         <div class="two wide column control volume-control">
-          <i :title="$gettext('Unmute')" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
-          <i :title="$gettext('Mute')" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
-          <i :title="$gettext('Mute')" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
+          <i
+            :title="labels.unmute"
+            @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
+          <i
+            :title="labels.mute"
+            @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
+          <i
+            :title="labels.mute"
+            @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
           <input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
         </div>
         <div class="two wide column control looping">
           <i
-            :title="$gettext('Looping disabled. Click to switch to single-track looping.')"
+            :title="labels.loopingDisabled"
             v-if="looping === 0"
             @click="$store.commit('player/looping', 1)"
             :disabled="!currentTrack"
             :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
           <i
-            :title="$gettext('Looping on a single track. Click to switch to whole queue looping.')"
+            :title="labels.loopingSingle"
             v-if="looping === 1"
             @click="$store.commit('player/looping', 2)"
             :disabled="!currentTrack"
@@ -102,7 +108,7 @@
             <span class="ui circular tiny orange label">1</span>
           </i>
           <i
-            :title="$gettext('Looping on whole queue. Click to disable looping.')"
+            :title="labels.loopingWhole"
             v-if="looping === 2"
             @click="$store.commit('player/looping', 0)"
             :disabled="!currentTrack"
@@ -111,7 +117,7 @@
         </div>
         <div
           :disabled="queue.tracks.length === 0"
-          :title="$gettext('Shuffle your queue')"
+          :title="labels.shuffle"
           class="two wide column control">
           <div v-if="isShuffling" class="ui inline shuffling inverted small active loader"></div>
           <i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
@@ -119,7 +125,7 @@
         <div class="one wide column"></div>
         <div
           :disabled="queue.tracks.length === 0"
-          :title="$gettext('Clear your queue')"
+          :title="labels.clear"
           class="two wide column control">
           <i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
@@ -236,6 +242,32 @@ export default {
       currentTimeFormatted: 'player/currentTimeFormatted',
       progress: 'player/progress'
     }),
+    labels () {
+      let previousTrack = this.$gettext('Previous track')
+      let play = this.$gettext('Play track')
+      let pause = this.$gettext('Pause track')
+      let next = this.$gettext('Next track')
+      let unmute = this.$gettext('Unmute')
+      let mute = this.$gettext('Mute')
+      let loopingDisabled = this.$gettext('Looping disabled. Click to switch to single-track looping.')
+      let loopingSingle = this.$gettext('Looping on a single track. Click to switch to whole queue looping.')
+      let loopingWhole = this.$gettext('Looping on whole queue. Click to disable looping.')
+      let shuffle = this.$gettext('Shuffle your queue')
+      let clear = this.$gettext('Clear your queue')
+      return {
+        previousTrack,
+        play,
+        pause,
+        next,
+        unmute,
+        mute,
+        loopingDisabled,
+        loopingSingle,
+        loopingWhole,
+        shuffle,
+        clear
+      }
+    },
     style: function () {
       let style = {
         'background': this.ambiantGradiant
diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue
index d04777ec..06ae2c1c 100644
--- a/front/src/components/audio/Search.vue
+++ b/front/src/components/audio/Search.vue
@@ -4,7 +4,7 @@
     <div :class="['ui', {'loading': isLoading }, 'search']">
       <div class="ui icon big input">
         <i class="search icon"></i>
-        <input ref="search" class="prompt" placeholder="Artist, album, track..." v-model.trim="query" type="text" />
+        <input ref="search" class="prompt" :placeholder="labels.searchPlaceholder" v-model.trim="query" type="text" />
       </div>
     </div>
     <template v-if="query.length > 0">
@@ -59,6 +59,13 @@ export default {
     }
     this.search()
   },
+  computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Artist, album, track...')
+      }
+    }
+  },
   methods: {
     search: _.debounce(function () {
       if (this.query.length < 1) {
diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue
index d5cb39e3..024a3690 100644
--- a/front/src/components/audio/SearchBar.vue
+++ b/front/src/components/audio/SearchBar.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="ui fluid category search">
     <slot></slot><div class="ui icon input">
-      <input class="prompt" placeholder="Search for artists, albums, tracks..." type="text">
+      <input class="prompt" :placeholder="labels.placeholder" type="text">
       <i class="search icon"></i>
     </div>
     <div class="results"></div>
@@ -14,7 +14,17 @@ import jQuery from 'jquery'
 import router from '@/router'
 
 export default {
+  computed: {
+    labels () {
+      return {
+        placeholder: this.$gettext('Search for artists, albums, tracks...')
+      }
+    }
+  },
   mounted () {
+    let artistLabel = this.$gettext('Artist')
+    let albumLabel = this.$gettext('Album')
+    let trackLabel = this.$gettext('Track')
     let self = this
     jQuery(this.$el).search({
       type: 'category',
@@ -39,7 +49,7 @@ export default {
             {
               code: 'artists',
               route: 'library.artists.detail',
-              name: 'Artist',
+              name: artistLabel,
               getTitle (r) {
                 return r.name
               },
@@ -50,7 +60,7 @@ export default {
             {
               code: 'albums',
               route: 'library.albums.detail',
-              name: 'Album',
+              name: albumLabel,
               getTitle (r) {
                 return r.title
               },
@@ -61,7 +71,7 @@ export default {
             {
               code: 'tracks',
               route: 'library.tracks.detail',
-              name: 'Track',
+              name: trackLabel,
               getTitle (r) {
                 return r.title
               },
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index b89734b0..2fdaaba4 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Log In'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2><translate>Log in to your Funkwhale account</translate></h2>
@@ -24,7 +24,7 @@
             required
             type="text"
             autofocus
-            placeholder="Enter your username or email"
+            :placeholder="labels.usernamePlaceholder"
             v-model="credentials.username"
             >
           </div>
@@ -72,6 +72,16 @@ export default {
   mounted () {
     this.$refs.username.focus()
   },
+  computed: {
+    labels () {
+      let usernamePlaceholder = this.$gettext('Enter your username or email')
+      let title = this.$gettext('Log In')
+      return {
+        usernamePlaceholder,
+        title
+      }
+    }
+  },
   methods: {
     submit () {
       var self = this
diff --git a/front/src/components/auth/Logout.vue b/front/src/components/auth/Logout.vue
index 32e21373..5f60a9db 100644
--- a/front/src/components/auth/Logout.vue
+++ b/front/src/components/auth/Logout.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Log Out'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2>
@@ -14,7 +14,13 @@
 
 <script>
 export default {
-  name: 'logout'
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Log Out')
+      }
+    }
+  }
 }
 </script>
 
diff --git a/front/src/components/auth/Profile.vue b/front/src/components/auth/Profile.vue
index 0ead7d74..1d15b206 100644
--- a/front/src/components/auth/Profile.vue
+++ b/front/src/components/auth/Profile.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="username + '\'s Profile'">
+  <div class="main pusher" v-title="labels.usernameProfile">
     <div v-if="isLoading" class="ui vertical segment">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
@@ -39,6 +39,13 @@ export default {
     this.$store.dispatch('auth/fetchProfile')
   },
   computed: {
+    labels () {
+      let msg = this.$gettext('%{ username }\'s profile')
+      let usernameProfile = this.$gettextInterpolate(msg, {username: this.username})
+      return {
+        usernameProfile
+      }
+    },
     signupDate () {
       let d = new Date(this.$store.state.auth.profile.date_joined)
       return dateFormat(d, 'longDate')
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
index 7ff825ef..5a74b888 100644
--- a/front/src/components/auth/Settings.vue
+++ b/front/src/components/auth/Settings.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Account Settings'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2 class="ui header">
@@ -63,7 +63,7 @@
             <translate>Change password</translate>
             <p slot="modal-header"><translate>Change your password?</translate></p>
             <div slot="modal-content">
-              <p>{{ $gettext("Changing your password will have the following consequences") }}</p>
+              <p><translate>Changing your password will have the following consequences</translate></p>
               <ul>
                 <li><translate>You will be logged out from this session and have to log out with the new one</translate></li>
                 <li><translate>Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password</translate></li>
@@ -175,6 +175,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Account Settings')
+      }
+    },
     orderedSettingsFields () {
       let self = this
       return this.settings.order.map(id => {
diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue
index 2a5cb8fb..ae3c47e5 100644
--- a/front/src/components/auth/Signup.vue
+++ b/front/src/components/auth/Signup.vue
@@ -1,8 +1,8 @@
 <template>
-  <div class="main pusher" v-title="'Sign Up'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2>{{ $gettext("Create a funkwhale account") }}</h2>
+        <h2><translate>Create a funkwhale account</translate></h2>
         <form
           :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
           @submit.prevent="submit()">
@@ -11,45 +11,45 @@
           </p>
 
           <div v-if="errors.length > 0" class="ui negative message">
-            <div class="header">{{ $gettext("We cannot create your account") }}</div>
+            <div class="header"><translate>We cannot create your account</translate></div>
             <ul class="list">
               <li v-for="error in errors">{{ error }}</li>
             </ul>
           </div>
           <div class="field">
-            <label>{{ $gettext("Username") }}</label>
+            <label><translate>Username</translate></label>
             <input
             ref="username"
             required
             type="text"
             autofocus
-            placeholder="Enter your username"
+            :placeholder="labels.usernamePlaceholder"
             v-model="username">
           </div>
           <div class="field">
-            <label>{{ $gettext("Email") }}</label>
+            <label><translate>Email</translate></label>
             <input
             ref="email"
             required
             type="email"
-            placeholder="Enter your email"
+            :placeholder="labels.emailPlaceholder"
             v-model="email">
           </div>
           <div class="field">
-            <label>{{ $gettext("Password") }}</label>
+            <label><translate>Password</translate></label>
             <password-input v-model="password" />
           </div>
           <div class="field">
-            <label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $gettext("Invitation code") }}</label>
-            <label v-else>{{ $gettext("Invitation code (optional)") }}</label>
+            <label v-if="!$store.state.instance.settings.users.registration_enabled.value"><translate>Invitation code</translate></label>
+            <label v-else><translate>Invitation code (optional)</translate></label>
             <input
             :required="!$store.state.instance.settings.users.registration_enabled.value"
             type="text"
-            :placeholder="$gettext('Enter your invitation code (case insensitive)')"
+            :placeholder="labels.placeholder"
             v-model="invitation">
           </div>
           <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">
-            {{ $gettext("Create my account") }}
+            <translate>Create my account</translate>
           </button>
         </form>
       </div>
@@ -89,6 +89,20 @@ export default {
       }
     })
   },
+  computed: {
+    labels () {
+      let title = this.$gettext('Sign Up')
+      let placeholder = this.$gettext('Enter your invitation code (case insensitive)')
+      let usernamePlaceholder = this.$gettext('Enter your username')
+      let emailPlaceholder = this.$gettext('Enter your email')
+      return {
+        title,
+        usernamePlaceholder,
+        emailPlaceholder,
+        placeholder
+      }
+    }
+  },
   methods: {
     submit () {
       var self = this
diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue
index 5c06766a..d74b0dfb 100644
--- a/front/src/components/common/ActionTable.vue
+++ b/front/src/components/common/ActionTable.vue
@@ -35,7 +35,7 @@
                     </translate>
                   </p>
                   <p slot="modal-content">
-                    {{ $gettext('This may affect a lot of elements, please double check this is really what you want.')}}
+                    <translate>This may affect a lot of elements, please double check this is really what you want.</translate>
                   </p>
                   <p slot="modal-confirm"><translate>Launch</translate></p>
                 </dangerous-button>
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index a231453f..2755dd19 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Your Favorites'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical center aligned stripe segment">
       <div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
         <div class="ui text loader">
@@ -103,6 +103,13 @@ export default {
   mounted () {
     $('.ui.dropdown').dropdown()
   },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Your Favorites')
+      }
+    }
+  },
   methods: {
     updateQueryString: function () {
       this.$router.replace({
diff --git a/front/src/components/federation/LibraryFollowTable.vue b/front/src/components/federation/LibraryFollowTable.vue
index 43085f61..6f2a4e4f 100644
--- a/front/src/components/federation/LibraryFollowTable.vue
+++ b/front/src/components/federation/LibraryFollowTable.vue
@@ -3,7 +3,7 @@
     <div class="ui form">
       <div class="fields">
         <div class="ui six wide field">
-          <input type="text" v-model="search" placeholder="Search by username, domain..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="ui four wide inline field">
           <div class="ui checkbox">
@@ -134,6 +134,13 @@ export default {
   created () {
     this.fetchData()
   },
+  computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by username, domain...')
+      }
+    }
+  },
   methods: {
     fetchData () {
       let params = _.merge({
diff --git a/front/src/components/federation/LibraryForm.vue b/front/src/components/federation/LibraryForm.vue
index 37d19e2f..7039cb52 100644
--- a/front/src/components/federation/LibraryForm.vue
+++ b/front/src/components/federation/LibraryForm.vue
@@ -20,7 +20,7 @@
         <label>
           <translate>Library name</translate>
         </label>
-        <input v-model="libraryUsername" type="text" placeholder="library@demo.funkwhale.audio" />
+        <input v-model="libraryUsername" type="text" :placeholder="labels.namePlaceholder" />
       </div>
       <div class="ui field">
         <label>&nbsp;</label>
@@ -91,6 +91,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        namePlaceholder: this.$gettext('library@demo.funkwhale.audio')
+      }
+    },
     scanErrors () {
       let errors = []
       if (!this.result) {
diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue
index 48d84942..64566396 100644
--- a/front/src/components/federation/LibraryTrackTable.vue
+++ b/front/src/components/federation/LibraryTrackTable.vue
@@ -4,7 +4,7 @@
       <div class="fields">
         <div class="ui field">
           <label><translate>Search</translate></label>
-          <input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="ui field">
           <label><translate>Import status</translate></label>
@@ -145,6 +145,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by title, artist, domain...')
+      }
+    },
     actionFilters () {
       var currentFilters = {
         q: this.search
diff --git a/front/src/components/forms/PasswordInput.vue b/front/src/components/forms/PasswordInput.vue
index 363f1f82..377b2d3e 100644
--- a/front/src/components/forms/PasswordInput.vue
+++ b/front/src/components/forms/PasswordInput.vue
@@ -6,7 +6,7 @@
     :type="passwordInputType"
     @input="$emit('input', $event.target.value)"
     :value="value">
-    <span @click="showPassword = !showPassword" :title="$gettext('Show/hide password')" class="ui icon button">
+    <span @click="showPassword = !showPassword" :title="labels.title" class="ui icon button">
       <i class="eye icon"></i>
     </span>
   </div>
@@ -20,6 +20,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Show/hide password')
+      }
+    },
     passwordInputType () {
       if (this.showPassword) {
         return 'text'
diff --git a/front/src/components/instance/Stats.vue b/front/src/components/instance/Stats.vue
index 4f145ba9..bb23b617 100644
--- a/front/src/components/instance/Stats.vue
+++ b/front/src/components/instance/Stats.vue
@@ -28,7 +28,7 @@
         </div>
       </div>
       <div class="column">
-        <h3 class="ui left aligned header">Library</h3>
+        <h3 class="ui left aligned header"><translate>Library</translate></h3>
         <div class="ui mini horizontal statistics">
           <div class="statistic">
             <div class="value">
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index b4cae319..ced69ede 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="'Album'">
+    <div v-if="isLoading" class="ui vertical segment" v-title="">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="album">
@@ -86,6 +86,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Album')
+      }
+    },
     wikipediaUrl () {
       return 'https://en.wikipedia.org/w/index.php?search=' + this.album.title + ' ' + this.album.artist.name
     },
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index ce147009..89eb7459 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="'Artist'">
+    <div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="artist">
@@ -103,6 +103,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Artist')
+      }
+    },
     totalTracks () {
       return this.albums.map((album) => {
         return album.tracks.length
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 9d02971c..b1374633 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Artists'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header">
         <translate>Browsing artists</translate>
@@ -10,7 +10,7 @@
             <label>
               <translate>Search</translate>
             </label>
-            <input type="text" v-model="query" placeholder="Enter an artist name..."/>
+            <input type="text" v-model="query" :placeholder="labels.searchPlaceholder"/>
           </div>
           <div class="field">
             <label><translate>Ordering</translate></label>
@@ -23,8 +23,8 @@
           <div class="field">
             <label><translate>Ordering direction</translate></label>
             <select class="ui dropdown" v-model="orderingDirection">
-              <option value="+">Ascending</option>
-              <option value="-">Descending</option>
+              <option value="+"><translate>Ascending</translate></option>
+              <option value="-"><translate>Descending</translate></option>
             </select>
           </div>
           <div class="field">
@@ -113,6 +113,16 @@ export default {
   mounted () {
     $('.ui.dropdown').dropdown()
   },
+  computed: {
+    labels () {
+      let searchPlaceholder = this.$gettext('Enter an artist name...')
+      let title = this.$gettext('Artists')
+      return {
+        searchPlaceholder,
+        title
+      }
+    }
+  },
   methods: {
     updateQueryString: _.debounce(function () {
       this.$router.replace({
diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue
index f38c0d64..5610198a 100644
--- a/front/src/components/library/Home.vue
+++ b/front/src/components/library/Home.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Home'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <search :autofocus="true"></search>
     </div>
@@ -60,6 +60,13 @@ export default {
   created () {
     this.fetchArtists()
   },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Home')
+      }
+    }
+  },
   methods: {
     fetchArtists () {
       var self = this
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
index 1438e2f4..49489a31 100644
--- a/front/src/components/library/Radios.vue
+++ b/front/src/components/library/Radios.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Radios'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header">
         <translate>Browsing radios</translate>
@@ -12,7 +12,7 @@
         <div class="fields">
           <div class="field">
             <label><translate>Search</translate></label>
-            <input type="text" v-model="query" placeholder="Enter a radio name..."/>
+            <input type="text" v-model="query" :placeholder="labels.searchPlaceholder"/>
           </div>
           <div class="field">
             <label><translate>Ordering</translate></label>
@@ -119,6 +119,16 @@ export default {
   mounted () {
     $('.ui.dropdown').dropdown()
   },
+  computed: {
+    labels () {
+      let searchPlaceholder = this.$gettext('Enter a radio name...')
+      let title = this.$gettext('Radios')
+      return {
+        searchPlaceholder,
+        title
+      }
+    }
+  },
   methods: {
     updateQueryString: _.debounce(function () {
       this.$router.replace({
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index d4ed13ad..01824292 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoadingTrack" class="ui vertical segment" v-title="'Track'">
+    <div v-if="isLoadingTrack" class="ui vertical segment" v-title="labels.title">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="track">
@@ -169,6 +169,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Track')
+      }
+    },
     wikipediaUrl () {
       return 'https://en.wikipedia.org/w/index.php?search=' + this.track.title + ' ' + this.track.artist.name
     },
diff --git a/front/src/components/library/import/ArtistImport.vue b/front/src/components/library/import/ArtistImport.vue
index 2f37edc6..f86f71cc 100644
--- a/front/src/components/library/import/ArtistImport.vue
+++ b/front/src/components/library/import/ArtistImport.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <h3 class="ui dividing block header">
-      <a :href="getMusicbrainzUrl('artist', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.name }}</a>
+      <a :href="getMusicbrainzUrl('artist', metadata.id)" target="_blank" :title="labels.viewOnMusicbrainz">{{ metadata.name }}</a>
     </h3>
     <form class="ui form" @submit.prevent="">
       <h6 class="ui header">
@@ -126,6 +126,11 @@ export default Vue.extend({
     }
   },
   computed: {
+    labels () {
+      return {
+        viewOnMusicbrainz: this.$gettext('View on MusicBrainz')
+      }
+    },
     type () {
       return 'artist'
     },
diff --git a/front/src/components/library/import/BatchDetail.vue b/front/src/components/library/import/BatchDetail.vue
index b0d64a3d..e46bf2a3 100644
--- a/front/src/components/library/import/BatchDetail.vue
+++ b/front/src/components/library/import/BatchDetail.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Import Batch #' + id">
+  <div v-title="labels.title">
     <div v-if="isLoading && !batch" class="ui vertical segment">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
@@ -47,7 +47,7 @@
                 v-if="stats.errored > 0"
                 class="ui tiny basic icon button">
                 <i class="redo icon" />
-                {{ $gettext('Rerun errored jobs')}}
+                <translate>Rerun errored jobs</translate>
               </button>
             </td>
           </tr>
@@ -61,7 +61,7 @@
         <div class="fields">
           <div class="ui field">
             <label><translate>Search</translate></label>
-            <input type="text" v-model="jobFilters.search" placeholder="Search by source..." />
+            <input type="text" v-model="jobFilters.search" :placeholder="labels.searchPlaceholder" />
           </div>
           <div class="ui field">
             <label><translate>Status</translate></label>
@@ -103,7 +103,7 @@
                 <button
                   @click="rerun({batches: [], jobs: [job.id]})"
                   v-if="job.status === 'errored'"
-                  :title="$gettext('Rerun job')"
+                  :title="labels.rerun"
                   class="ui tiny basic icon button">
                   <i class="redo icon" />
                 </button>
@@ -180,6 +180,19 @@ export default {
       clearTimeout(this.timeout)
     }
   },
+  computed: {
+    labels () {
+      let msg = this.$gettext('Import Batch #%{ id }')
+      let title = this.$gettextInterpolate(msg, {id: this.id})
+      let rerun = this.$gettext('Rerun job')
+      let searchPlaceholder = this.$gettext('Search by source...')
+      return {
+        title,
+        searchPlaceholder,
+        rerun
+      }
+    }
+  },
   methods: {
     fetchData () {
       var self = this
diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue
index a7a7de91..ff26d0f7 100644
--- a/front/src/components/library/import/BatchList.vue
+++ b/front/src/components/library/import/BatchList.vue
@@ -1,12 +1,12 @@
 <template>
-  <div v-title="'Import Batches'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div v-if="isLoading" :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
       <div class="ui inline form">
         <div class="fields">
           <div class="ui field">
             <label><translate>Search</translate></label>
-            <input type="text" v-model="filters.search" placeholder="Search by submitter, source..." />
+            <input type="text" v-model="filters.search" :placeholder="labels.searchPlaceholder" />
           </div>
           <div class="ui field">
             <label><translate>Status</translate></label>
@@ -111,6 +111,16 @@ export default {
   created () {
     this.fetchData()
   },
+  computed: {
+    labels () {
+      let searchPlaceholder = this.$gettext('Search by submitter, source...')
+      let title = this.$gettext('Import Batches')
+      return {
+        searchPlaceholder,
+        title
+      }
+    }
+  },
   methods: {
     fetchData () {
       let params = {
diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue
index b24bb8eb..e7d9d7d3 100644
--- a/front/src/components/library/import/Main.vue
+++ b/front/src/components/library/import/Main.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Import Music'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <div class="ui top three attached ordered steps">
         <a @click="currentStep = 0" :class="['step', {'active': currentStep === 0}, {'completed': currentStep > 0}]">
@@ -112,7 +112,7 @@
             <p><translate>You can also skip this step and enter metadata manually.</translate></p>
           </div>
           <div class="column">
-            <h5 class="ui header">What is metadata?</h5>
+            <h5 class="ui header"><translate>What is metadata?</translate></h5>
             <template v-translate>
               Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the
               <a href="https://musicbrainz.org" target="_blank">
@@ -262,6 +262,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        title: this.$gettext('Import Music')
+      }
+    },
     metadataComponent () {
       if (this.currentType === 'artist') {
         return 'ArtistCard'
diff --git a/front/src/components/library/radios/Builder.vue b/front/src/components/library/radios/Builder.vue
index 2b8e71a3..dbcd376b 100644
--- a/front/src/components/library/radios/Builder.vue
+++ b/front/src/components/library/radios/Builder.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="ui vertical stripe segment" v-title="'Radio Builder'">
+  <div class="ui vertical stripe segment" v-title="labels.title">
     <div>
       <div>
         <h2 class="ui header">
@@ -10,7 +10,7 @@
           <div class="inline fields">
             <div class="field">
               <label for="name"><translate>Radio name</translate></label>
-              <input id="name" type="text" v-model="radioName" placeholder="My awesome radio" />
+              <input id="name" type="text" v-model="radioName" :placeholder="labels.placeholder" />
             </div>
             <div class="field">
               <input id="public" type="checkbox" v-model="isPublic" />
@@ -201,6 +201,14 @@ export default {
     }
   },
   computed: {
+    labels () {
+      let title = this.$gettext('Radio Builder')
+      let placeholder = this.$gettext('My awesome radio')
+      return {
+        title,
+        placeholder
+      }
+    },
     canSave: function () {
       return (
         this.radioName.length > 0 && this.checkErrors.length === 0
diff --git a/front/src/components/manage/library/FilesTable.vue b/front/src/components/manage/library/FilesTable.vue
index 98b23f06..9bec0714 100644
--- a/front/src/components/manage/library/FilesTable.vue
+++ b/front/src/components/manage/library/FilesTable.vue
@@ -4,7 +4,7 @@
       <div class="fields">
         <div class="ui field">
           <label><translate>Search</translate></label>
-          <input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
           <label><translate>Ordering</translate></label>
@@ -17,8 +17,8 @@
         <div class="field">
           <label><translate>Ordering direction</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+">Ascending</option>
-            <option value="-">Descending</option>
+            <option value="+"><translate>Ascending</translate></option>
+            <option value="-"><translate>Descending</translate></option>
           </select>
         </div>
       </div>
@@ -170,6 +170,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by title, artist, domain...')
+      }
+    },
     actionFilters () {
       var currentFilters = {
         q: this.search
diff --git a/front/src/components/manage/library/RequestsTable.vue b/front/src/components/manage/library/RequestsTable.vue
index 4c2883ff..be25c4bb 100644
--- a/front/src/components/manage/library/RequestsTable.vue
+++ b/front/src/components/manage/library/RequestsTable.vue
@@ -4,7 +4,7 @@
       <div class="fields">
         <div class="ui field">
           <label><translate>Search</translate></label>
-          <input type="text" v-model="search" placeholder="Search by artist, username, comment..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
           <label><translate>Ordering</translate></label>
@@ -17,12 +17,12 @@
         <div class="field">
           <label><translate>Ordering direction</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+">Ascending</option>
-            <option value="-">Descending</option>
+            <option value="+"><translate>Ascending</translate></option>
+            <option value="-"><translate>Descending</translate></option>
           </select>
         </div>
         <div class="field">
-          <label>{{ $gettext("Status") }}</label>
+          <label><translate>Status</translate></label>
           <select class="ui dropdown" v-model="status">
             <option :value="null"><translate>All</translate></option>
             <option :value="'pending'"><translate>Pending</translate></option>
@@ -175,6 +175,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by artist, username, comment...')
+      }
+    },
     actionFilters () {
       var currentFilters = {
         q: this.search
diff --git a/front/src/components/manage/users/InvitationForm.vue b/front/src/components/manage/users/InvitationForm.vue
index 92e419bd..8ee5d052 100644
--- a/front/src/components/manage/users/InvitationForm.vue
+++ b/front/src/components/manage/users/InvitationForm.vue
@@ -9,8 +9,8 @@
       </div>
       <div class="inline fields">
         <div class="ui field">
-          <label>{{ $gettext('Invitation code')}}</label>
-          <input type="text" v-model="code" :placeholder="$gettext('Leave empty for a random code')" />
+          <label><translate>Invitation code</translate></label>
+          <input type="text" v-model="code" :placeholder="labels.placeholder" />
         </div>
         <div class="ui field">
           <button :class="['ui', {loading: isLoading}, 'button']" :disabled="isLoading" type="submit">
@@ -52,6 +52,13 @@ export default {
       errors: []
     }
   },
+  computed: {
+    labels () {
+      return {
+        placeholder: this.$gettext('Leave empty for a random code')
+      }
+    }
+  },
   methods: {
     submit () {
       let self = this
diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue
index c9700f20..7d4864e8 100644
--- a/front/src/components/manage/users/InvitationsTable.vue
+++ b/front/src/components/manage/users/InvitationsTable.vue
@@ -4,10 +4,10 @@
       <div class="fields">
         <div class="ui field">
           <label><translate>Search</translate></label>
-          <input type="text" v-model="search" placeholder="Search by username, email, code..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
-          <label>{{ $gettext("Ordering") }}</label>
+          <label><translate>Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ option[1] }}
@@ -15,7 +15,7 @@
           </select>
         </div>
         <div class="field">
-          <label>{{ $gettext("Status") }}</label>
+          <label><translate>Status</translate></label>
           <select class="ui dropdown" v-model="isOpen">
             <option :value="null"><translate>All</translate></option>
             <option :value="true"><translate>Open</translate></option>
@@ -147,6 +147,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by username, email, code...')
+      }
+    },
     actionFilters () {
       var currentFilters = {
         q: this.search
diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue
index d33261f1..a2b486e2 100644
--- a/front/src/components/manage/users/UsersTable.vue
+++ b/front/src/components/manage/users/UsersTable.vue
@@ -4,7 +4,7 @@
       <div class="fields">
         <div class="ui field">
           <label><translate>Search</translate></label>
-          <input type="text" v-model="search" placeholder="Search by username, email, name..." />
+          <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
           <label><translate>Ordering</translate></label>
@@ -157,6 +157,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$gettext('Search by username, email, name...')
+      }
+    },
     privacyLevels () {
       return {}
     },
diff --git a/front/src/components/metadata/ArtistCard.vue b/front/src/components/metadata/ArtistCard.vue
index 8f9aa741..ef2fc616 100644
--- a/front/src/components/metadata/ArtistCard.vue
+++ b/front/src/components/metadata/ArtistCard.vue
@@ -6,7 +6,7 @@
       </div>
       <template v-if="data.id">
         <div class="header">
-          <a :href="getMusicbrainzUrl('artist', data.id)" target="_blank" title="View on MusicBrainz">{{ data.name }}</a>
+          <a :href="getMusicbrainzUrl('artist', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.name }}</a>
         </div>
         <div class="description">
           <table class="ui very basic fixed single line compact table">
@@ -16,7 +16,7 @@
                   {{ group['first-release-date'] }}
                 </td>
                 <td colspan="3">
-                  <a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="$gettext('View on MusicBrainz')">
+                  <a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="labels.musicbrainz">
                     {{ group.title }}
                   </a>
                 </td>
@@ -44,6 +44,11 @@ export default Vue.extend({
     }
   },
   computed: {
+    labels () {
+      return {
+        musicbrainz: this.$gettext('View on MusicBrainz')
+      }
+    },
     type () {
       return 'artist'
     },
diff --git a/front/src/components/metadata/ReleaseCard.vue b/front/src/components/metadata/ReleaseCard.vue
index 13f5b53a..0da2c7e2 100644
--- a/front/src/components/metadata/ReleaseCard.vue
+++ b/front/src/components/metadata/ReleaseCard.vue
@@ -6,10 +6,10 @@
       </div>
       <template v-if="data.id">
         <div class="header">
-          <a :href="getMusicbrainzUrl('release', data.id)" target="_blank" title="View on MusicBrainz">{{ data.title }}</a>
+          <a :href="getMusicbrainzUrl('release', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.title }}</a>
         </div>
         <div class="meta">
-          <a :href="getMusicbrainzUrl('artist', data['artist-credit'][0]['artist']['id'])" target="_blank" title="View on MusicBrainz">{{ data['artist-credit-phrase'] }}</a>
+          <a :href="getMusicbrainzUrl('artist', data['artist-credit'][0]['artist']['id'])" target="_blank" :title="labels.musicbrainz">{{ data['artist-credit-phrase'] }}</a>
         </div>
         <div class="description">
           <table class="ui very basic fixed single line compact table">
@@ -19,7 +19,7 @@
                   {{ track.position }}
                 </td>
                 <td colspan="3">
-                  <a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="$gettext('View on MusicBrainz')">
+                  <a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="labels.musicbrainz">
                     {{ track.recording.title }}
                   </a>
                 </td>
@@ -48,6 +48,11 @@ export default Vue.extend({
     }
   },
   computed: {
+    labels () {
+      return {
+        musicbrainz: this.$gettext('View on MusicBrainz')
+      }
+    },
     type () {
       return 'release'
     },
diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue
index 1ba30b7c..4e8bf357 100644
--- a/front/src/components/metadata/Search.vue
+++ b/front/src/components/metadata/Search.vue
@@ -12,7 +12,7 @@
     </div>
     <div class="ui fluid search">
       <div class="ui icon input">
-        <input class="prompt" :placeholder="$gettext('Enter your search query...')" type="text">
+        <input class="prompt" :placeholder="labels.placeholder" type="text">
         <i class="search icon"></i>
       </div>
       <div class="results"></div>
@@ -109,6 +109,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        placeholder: this.$gettext('Enter your search query...')
+      }
+    },
     currentTypeObject: function () {
       let self = this
       return this.types.filter(t => {
diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue
index 6af726be..a480975e 100644
--- a/front/src/components/playlists/Card.vue
+++ b/front/src/components/playlists/Card.vue
@@ -24,7 +24,7 @@
           %{ count} track
         </translate>
       </span>
-      <play-button class="mini basic orange right floated" :playlist="playlist">Play all</play-button>
+      <play-button class="mini basic orange right floated" :playlist="playlist"><translate>Play all</translate></play-button>
     </div>
   </div>
 </template>
diff --git a/front/src/components/playlists/Editor.vue b/front/src/components/playlists/Editor.vue
index d7ae04b2..35374cb0 100644
--- a/front/src/components/playlists/Editor.vue
+++ b/front/src/components/playlists/Editor.vue
@@ -27,7 +27,7 @@
         @click="insertMany(queueTracks)"
         :disabled="queueTracks.length === 0"
         :class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
-        title="Copy tracks from current queue to playlist">
+        :title="labels.copyTitle">
           <i class="plus icon"></i>
           <translate
             translate-plural="Insert from queue (%{ count } tracks)"
@@ -47,7 +47,7 @@
       </dangerous-button>
       <div class="ui hidden divider"></div>
       <template v-if="plts.length > 0">
-        <p>Drag and drop rows to reorder tracks in the playlist</p>
+        <p><translate>Drag and drop rows to reorder tracks in the playlist</translate></p>
         <table class="ui compact very basic fixed single line unstackable table">
           <draggable v-model="plts" element="tbody" @update="reorder">
             <tr v-for="(plt, index) in plts" :key="plt.id">
@@ -158,6 +158,11 @@ export default {
     ...mapState({
       queueTracks: state => state.queue.tracks
     }),
+    labels () {
+      return {
+        copyTitle: this.$gettext('Copy tracks from current queue to playlist')
+      }
+    },
     status () {
       if (this.isLoading) {
         return 'loading'
diff --git a/front/src/components/playlists/Form.vue b/front/src/components/playlists/Form.vue
index cbde4f8e..34c75eb9 100644
--- a/front/src/components/playlists/Form.vue
+++ b/front/src/components/playlists/Form.vue
@@ -20,7 +20,7 @@
     <div class="three fields">
       <div class="field">
         <label><translate>Playlist name</translate></label>
-        <input v-model="name" required type="text" placeholder="My awesome playlist" />
+        <input v-model="name" required type="text" :placeholder="labels.placeholder" />
       </div>
       <div class="field">
         <label><translate>Playlist visibility</translate></label>
@@ -69,6 +69,11 @@ export default {
     return d
   },
   computed: {
+    labels () {
+      return {
+        placeholder: this.$gettext('My awesome playlist')
+      }
+    },
     privacyLevelChoices: function () {
       return [
         {
diff --git a/front/src/components/playlists/PlaylistModal.vue b/front/src/components/playlists/PlaylistModal.vue
index cee32dab..668dbf75 100644
--- a/front/src/components/playlists/PlaylistModal.vue
+++ b/front/src/components/playlists/PlaylistModal.vue
@@ -50,7 +50,7 @@
                 <div
                   v-if="track"
                   class="ui green icon basic small right floated button"
-                  :title="$gettext('Add to this playlist')"
+                  :title="labels.addToPlaylist"
                   @click="addToPlaylist(playlist.id)">
                   <i class="plus icon"></i> <translate>Add track</translate>
                 </div>
@@ -110,6 +110,11 @@ export default {
       playlists: state => state.playlists.playlists,
       track: state => state.playlists.modalTrack
     }),
+    labels () {
+      return {
+        addToPlaylist: this.$gettext('Add to this playlist')
+      }
+    },
     sortedPlaylists () {
       let p = _.sortBy(this.playlists, [(e) => { return e.modification_date }])
       p.reverse()
diff --git a/front/src/components/playlists/TrackPlaylistIcon.vue b/front/src/components/playlists/TrackPlaylistIcon.vue
index 4883ba41..a75c217f 100644
--- a/front/src/components/playlists/TrackPlaylistIcon.vue
+++ b/front/src/components/playlists/TrackPlaylistIcon.vue
@@ -10,7 +10,7 @@
     v-else
     @click="$store.commit('playlists/chooseTrack', track)"
     :class="['playlist-icon', 'list', 'link', 'icon']"
-    :title="$gettext('Add to playlist...')">
+    :title="labels.addToPlaylist">
   </i>
 </template>
 
@@ -25,6 +25,13 @@ export default {
     return {
       showModal: false
     }
+  },
+  computed: {
+    labels () {
+      return {
+        addToPlaylist: this.$gettext('Add to playlist...')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/components/requests/Form.vue b/front/src/components/requests/Form.vue
index 0b96c20e..b6752a63 100644
--- a/front/src/components/requests/Form.vue
+++ b/front/src/components/requests/Form.vue
@@ -4,21 +4,21 @@
       <p><translate>Something's missing in the library? Let us know what you would like to listen!</translate></p>
       <div class="required field">
         <label><translate>Artist name</translate></label>
-        <input v-model="currentArtistName" placeholder="The Beatles, Mickael Jackson…" required maxlength="200">
+        <input v-model="currentArtistName" :placeholder="labels.artistNamePlaceholder" required maxlength="200">
       </div>
       <div class="field">
         <label><translate>Albums</translate></label>
         <p><translate>Leave this field empty if you're requesting the whole discography.</translate></p>
-        <input v-model="currentAlbums" placeholder="The White Album, Thriller…" maxlength="2000">
+        <input v-model="currentAlbums" :placeholder="labels.albumTitlePlaceholder" maxlength="2000">
       </div>
       <div class="field">
         <label><translate>Comment</translate></label>
-        <textarea v-model="currentComment" rows="3" placeholder="Use this comment box to add details to your request if needed" maxlength="2000"></textarea>
+        <textarea v-model="currentComment" rows="3" :placeholder="comentPlaceholder" maxlength="2000"></textarea>
       </div>
       <button class="ui submit button" type="submit"><translate>Submit</translate></button>
     </form>
     <div v-else class="ui success message">
-      <div class="header">Request submitted!</div>
+      <div class="header"><translate>Request submitted!</translate></div>
       <p><translate>We've received your request, you'll get some groove soon ;)</translate></p>
       <button @click="reset" class="ui button"><translate>Submit another request</translate></button>
     </div>
@@ -68,6 +68,18 @@ export default {
       requests: []
     }
   },
+  computed: {
+    labels () {
+      let artistNamePlaceholder = this.$gettext('The Beatles, Mickael Jackson…')
+      let albumTitlePlaceholder = this.$gettext('The White Album, Thriller…')
+      let commentPlaceholder = this.$gettext('Use this comment box to add details to your request if needed')
+      return {
+        artistNamePlaceholder,
+        albumTitlePlaceholder,
+        commentPlaceholder
+      }
+    }
+  },
   methods: {
     fetchRequests () {
       let self = this
diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue
index 016ef6f7..d21b4e27 100644
--- a/front/src/views/admin/Settings.vue
+++ b/front/src/views/admin/Settings.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher"  v-title="$gettext('Instance settings')">
+  <div class="main pusher"  v-title="labels.settings">
     <div class="ui vertical stripe segment">
       <div class="ui text container">
         <div :class="['ui', {'loading': isLoading}, 'form']"></div>
@@ -70,6 +70,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        settings: this.$gettext('Instance settings')
+      }
+    },
     groups () {
       // somehow, extraction fails if in the return block directly
       let instanceLabel = this.$gettext('Instance information')
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
index a592b4f3..08277a24 100644
--- a/front/src/views/admin/library/Base.vue
+++ b/front/src/views/admin/library/Base.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher"  v-title="'Manage library'">
+  <div class="main pusher"  v-title="labels.title">
     <div class="ui secondary pointing menu">
       <router-link
         class="ui item"
@@ -10,7 +10,7 @@
           <translate>Import requests</translate>
           <div
             :class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']"
-            :title="$gettext('Pending import requests')">
+            :title="labels.pendingRequests">
             {{ $store.state.ui.notifications.importRequests }}</div>
           </router-link>
     </div>
@@ -19,7 +19,18 @@
 </template>
 
 <script>
-export default {}
+export default {
+  computed: {
+    labels () {
+      let title = this.$gettext('Manage library')
+      let pendingRequests = this.$gettext('Pending import requests')
+      return {
+        title,
+        pendingRequests
+      }
+    }
+  }
+}
 </script>
 
 <style lang="scss">
diff --git a/front/src/views/admin/library/FilesList.vue b/front/src/views/admin/library/FilesList.vue
index aa397f94..1c5216c8 100644
--- a/front/src/views/admin/library/FilesList.vue
+++ b/front/src/views/admin/library/FilesList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Files'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Library files</translate></h2>
       <div class="ui hidden divider"></div>
@@ -14,6 +14,13 @@ import LibraryFilesTable from '@/components/manage/library/FilesTable'
 export default {
   components: {
     LibraryFilesTable
+  },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Files')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/admin/library/RequestsList.vue b/front/src/views/admin/library/RequestsList.vue
index 781f38ca..d945fb86 100644
--- a/front/src/views/admin/library/RequestsList.vue
+++ b/front/src/views/admin/library/RequestsList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="$gettext('Import requests')">
+  <div v-title="labels.importRequests">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Import requests</translate></h2>
       <div class="ui hidden divider"></div>
@@ -14,6 +14,13 @@ import LibraryRequestsTable from '@/components/manage/library/RequestsTable'
 export default {
   components: {
     LibraryRequestsTable
+  },
+  computed: {
+    labels () {
+      return {
+        importRequests: this.$gettext('Import requests')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/admin/users/Base.vue b/front/src/views/admin/users/Base.vue
index d0426497..bbdf4432 100644
--- a/front/src/views/admin/users/Base.vue
+++ b/front/src/views/admin/users/Base.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher"  v-title="$gettext('Manage users')">
+  <div class="main pusher"  v-title="labels.manageUsers">
     <div class="ui secondary pointing menu">
       <router-link
         class="ui item"
@@ -13,7 +13,15 @@
 </template>
 
 <script>
-export default {}
+export default {
+  computed: {
+    labels () {
+      return {
+        manageUsers: this.$gettext('Manage users')
+      }
+    }
+  }
+}
 </script>
 
 <style lang="scss">
diff --git a/front/src/views/admin/users/InvitationsList.vue b/front/src/views/admin/users/InvitationsList.vue
index b8744561..b2dd8f03 100644
--- a/front/src/views/admin/users/InvitationsList.vue
+++ b/front/src/views/admin/users/InvitationsList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="$gettext('Invitations')">
+  <div v-title="labels.invitations">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Invitations</translate></h2>
       <invitation-form></invitation-form>
@@ -17,6 +17,13 @@ export default {
   components: {
     InvitationForm,
     InvitationsTable
+  },
+  computed: {
+    labels () {
+      return {
+        invitations: this.$gettext('Invitations')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/admin/users/UsersDetail.vue b/front/src/views/admin/users/UsersDetail.vue
index a97eb8d5..72efabd8 100644
--- a/front/src/views/admin/users/UsersDetail.vue
+++ b/front/src/views/admin/users/UsersDetail.vue
@@ -53,7 +53,7 @@
               <tr>
                 <td>
                   <translate>Account active</translate>
-                  <span :data-tooltip="$gettext('Determine if the user account is active or not. Inactive users cannot login or user the service.')"><i class="question circle icon"></i></span>
+                  <span :data-tooltip="labels.inactive"><i class="question circle icon"></i></span>
                 </td>
                 <td>
                   <div class="ui toggle checkbox">
@@ -141,6 +141,11 @@ export default {
     }
   },
   computed: {
+    labels () {
+      return {
+        inactive: this.$gettext('Determine if the user account is active or not. Inactive users cannot login or user the service.')
+      }
+    },
     allPermissions () {
       return [
         {
diff --git a/front/src/views/admin/users/UsersList.vue b/front/src/views/admin/users/UsersList.vue
index edb9e272..ef4d6096 100644
--- a/front/src/views/admin/users/UsersList.vue
+++ b/front/src/views/admin/users/UsersList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="$gettext('Users')">
+  <div v-title="labels.users">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Users</translate></h2>
       <div class="ui hidden divider"></div>
@@ -14,6 +14,13 @@ import UsersTable from '@/components/manage/users/UsersTable'
 export default {
   components: {
     UsersTable
+  },
+  computed: {
+    labels () {
+      return {
+        users: this.$gettext('Users')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/auth/EmailConfirm.vue b/front/src/views/auth/EmailConfirm.vue
index 1fb99961..7b982504 100644
--- a/front/src/views/auth/EmailConfirm.vue
+++ b/front/src/views/auth/EmailConfirm.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="$gettext('Confirm your email')">
+  <div class="main pusher" v-title="labels.confirm">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2><translate>Confirm your email</translate></h2>
@@ -45,6 +45,13 @@ export default {
       success: false
     }
   },
+  computed: {
+    labels () {
+      return {
+        confirm: this.$gettext('Confirm your email')
+      }
+    }
+  },
   methods: {
     submit () {
       let self = this
diff --git a/front/src/views/auth/PasswordReset.vue b/front/src/views/auth/PasswordReset.vue
index 4775e024..52787a51 100644
--- a/front/src/views/auth/PasswordReset.vue
+++ b/front/src/views/auth/PasswordReset.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="$gettext('Reset your password')">
+  <div class="main pusher" v-title="labels.reset">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2><translate>Reset your password</translate></h2>
@@ -18,7 +18,7 @@
               ref="email"
               type="email"
               autofocus
-              :placeholder="$gettext('Input the email address binded to your account')"
+              :placeholder="labels.placeholder"
               v-model="email">
           </div>
           <router-link :to="{path: '/login'}">
@@ -47,6 +47,16 @@ export default {
   mounted () {
     this.$refs.email.focus()
   },
+  computed: {
+    labels () {
+      let reset = this.$gettext('Reset your password')
+      let placeholder = this.$gettext('Input the email address binded to your account')
+      return {
+        reset,
+        placeholder
+      }
+    }
+  },
   methods: {
     submit () {
       let self = this
diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue
index 42f2e178..b6e4f232 100644
--- a/front/src/views/auth/PasswordResetConfirm.vue
+++ b/front/src/views/auth/PasswordResetConfirm.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="$gettext('Change your password')">
+  <div class="main pusher" v-title="labels.changePassword">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2><translate>Change your password</translate></h2>
@@ -56,6 +56,13 @@ export default {
       success: false
     }
   },
+  computed: {
+    labels () {
+      return {
+        changePassword: this.$gettext('Change your password')
+      }
+    }
+  },
   methods: {
     submit () {
       let self = this
diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue
index 9ab5dc2b..908ab790 100644
--- a/front/src/views/federation/Base.vue
+++ b/front/src/views/federation/Base.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher"  v-title="'Federation'">
+  <div class="main pusher"  v-title="labels.title">
     <div class="ui secondary pointing menu">
       <router-link
         class="ui item"
@@ -12,7 +12,7 @@
             class="ui item"
             :to="{name: 'federation.followers.list'}">
             <translate>Followers</translate>
-            <div class="ui teal label" :title="$gettext('Pending requests')">{{ requestsCount }}</div>
+            <div class="ui teal label" :title="labels.pendingRequests">{{ requestsCount }}</div>
           </router-link>
         </div>
     </div>
@@ -30,6 +30,16 @@ export default {
   created () {
     this.fetchRequestsCount()
   },
+  computed: {
+    labels () {
+      let title = this.$gettext('Federation')
+      let pendingRequests = this.$gettext('Pending requests')
+      return {
+        title,
+        pendingRequests
+      }
+    }
+  },
   methods: {
     fetchRequestsCount () {
       let self = this
diff --git a/front/src/views/federation/LibraryDetail.vue b/front/src/views/federation/LibraryDetail.vue
index 22399b0b..4ddccb79 100644
--- a/front/src/views/federation/LibraryDetail.vue
+++ b/front/src/views/federation/LibraryDetail.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="'Library'">
+    <div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="object">
@@ -20,7 +20,7 @@
               <tr>
                 <td >
                   <translate>Follow status</translate>
-                  <span :data-tooltip="$gettext('This indicate if the remote library granted you access')"><i class="question circle icon"></i></span>
+                  <span :data-tooltip="labels.statusTooltip"><i class="question circle icon"></i></span>
                 </td>
                 <td>
                   <template v-if="object.follow.approved === null">
@@ -39,7 +39,7 @@
               <tr>
                 <td>
                   <translate>Federation</translate>
-                  <span :data-tooltip="$gettext('Use this flag to enable/disable federation with this library')"><i class="question circle icon"></i></span>
+                  <span :data-tooltip="labels.federationTooltip"><i class="question circle icon"></i></span>
                 </td>
                 <td>
                   <div class="ui toggle checkbox">
@@ -55,7 +55,7 @@
               <tr>
                 <td>
                   <translate>Auto importing</translate>
-                  <span :data-tooltip="$gettext('When enabled, auto importing will automatically import new tracks published in this library')"><i class="question circle icon"></i></span>
+                  <span :data-tooltip="labels.autoImportTooltip"><i class="question circle icon"></i></span>
                 </td>
                 <td>
                   <div class="ui toggle checkbox">
@@ -69,7 +69,7 @@
               </tr>
               <!-- Disabled until properly implemented on the backend
               <tr>
-                <td>File mirroring</td>
+                <td><translate>File mirroring</translate></td>
                 <td>
                   <div class="ui toggle checkbox">
                     <input
@@ -190,6 +190,18 @@ export default {
     }
   },
   computed: {
+    labels () {
+      let title = this.$gettext('Library')
+      let statusTooltip = this.$gettext('This indicate if the remote library granted you access')
+      let federationTooltip = this.$gettext('Use this flag to enable/disable federation with this library')
+      let autoImportTooltip = this.$gettext('When enabled, auto importing will automatically import new tracks published in this library')
+      return {
+        title,
+        statusTooltip,
+        federationTooltip,
+        autoImportTooltip
+      }
+    },
     libraryUsername () {
       let actor = this.object.actor
       return `${actor.preferred_username}@${actor.domain}`
diff --git a/front/src/views/federation/LibraryFollowersList.vue b/front/src/views/federation/LibraryFollowersList.vue
index 28e2a999..0a9267a8 100644
--- a/front/src/views/federation/LibraryFollowersList.vue
+++ b/front/src/views/federation/LibraryFollowersList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Followers'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Browsing followers</translate></h2>
       <p>
@@ -17,6 +17,13 @@ import LibraryFollowTable from '@/components/federation/LibraryFollowTable'
 export default {
   components: {
     LibraryFollowTable
+  },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Followers')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/federation/LibraryList.vue b/front/src/views/federation/LibraryList.vue
index ad54c008..c043d17b 100644
--- a/front/src/views/federation/LibraryList.vue
+++ b/front/src/views/federation/LibraryList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Libraries'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Browsing libraries</translate></h2>
       <router-link
@@ -13,7 +13,7 @@
         <div class="fields">
           <div class="field">
             <label><translate>Search</translate></label>
-            <input class="search" type="text" v-model="query" placeholder="Enter an library domain name..."/>
+            <input class="search" type="text" v-model="query" :placeholder="labels.searchPlaceholder"/>
           </div>
           <div class="field">
             <label><translate>Ordering</translate></label>
@@ -117,6 +117,16 @@ export default {
     $('.ui.dropdown').dropdown()
     $(this.$el).find('.field .search').focus()
   },
+  computed: {
+    labels () {
+      let searchPlaceholder = this.$gettext('Enter an library domain name...')
+      let title = this.$gettext('Libraries')
+      return {
+        searchPlaceholder,
+        title
+      }
+    }
+  },
   methods: {
     updateQueryString: _.debounce(function () {
       this.$router.replace({
diff --git a/front/src/views/federation/LibraryTrackList.vue b/front/src/views/federation/LibraryTrackList.vue
index 5bfce3f8..55f9e46a 100644
--- a/front/src/views/federation/LibraryTrackList.vue
+++ b/front/src/views/federation/LibraryTrackList.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="'Federated tracks'">
+  <div v-title="labels.title">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Browsing federated tracks</translate></h2>
       <div class="ui hidden divider"></div>
@@ -14,6 +14,13 @@ import LibraryTrackTable from '@/components/federation/LibraryTrackTable'
 export default {
   components: {
     LibraryTrackTable
+  },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Federated tracks')
+      }
+    }
   }
 }
 </script>
diff --git a/front/src/views/instance/Timeline.vue b/front/src/views/instance/Timeline.vue
index 5e9a6efa..a70bd404 100644
--- a/front/src/views/instance/Timeline.vue
+++ b/front/src/views/instance/Timeline.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="main pusher" v-title="'Instance Timeline'">
+  <div class="main pusher" v-title="labels.title">
     <div class="ui vertical center aligned stripe segment">
       <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
         <div class="ui text loader"><translate>Loading timeline...</translate></div>
@@ -51,7 +51,12 @@ export default {
   computed: {
     ...mapState({
       events: state => state.instance.events
-    })
+    }),
+    labels () {
+      return {
+        title: this.$gettext('Instance Timeline')
+      }
+    }
   },
   methods: {
     fetchEvents () {
diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue
index 5074ce87..3fd4730b 100644
--- a/front/src/views/playlists/Detail.vue
+++ b/front/src/views/playlists/Detail.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="$gettext('Playlist')">
+    <div v-if="isLoading" class="ui vertical segment" v-title="labels.playlist">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <div v-if="!isLoading && playlist" class="ui head vertical center aligned stripe segment" v-title="playlist.name">
@@ -47,7 +47,7 @@
           :playlist="playlist" :playlist-tracks="playlistTracks"></playlist-editor>
       </template>
       <template v-else>
-        <h2>Tracks</h2>
+        <h2><translate>Tracks</translate></h2>
         <track-table :display-position="true" :tracks="tracks"></track-table>
       </template>
     </div>
@@ -83,6 +83,13 @@ export default {
   created: function () {
     this.fetch()
   },
+  computed: {
+    labels () {
+      return {
+        playlist: this.$gettext('Playlist')
+      }
+    }
+  },
   methods: {
     updatePlts (v) {
       this.playlistTracks = v
diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue
index 6e39e73f..c28f9956 100644
--- a/front/src/views/playlists/List.vue
+++ b/front/src/views/playlists/List.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-title="$gettext('Playlists')">
+  <div v-title="labels.playlists">
     <div class="ui vertical stripe segment">
       <h2 class="ui header"><translate>Browsing playlists</translate></h2>
       <div :class="['ui', {'loading': isLoading}, 'form']">
@@ -12,7 +12,7 @@
         <div class="fields">
           <div class="field">
             <label><translate>Search</translate></label>
-            <input type="text" v-model="query" :placeholder="$gettext('Enter an playlist name...')"/>
+            <input type="text" v-model="query" :placeholder="labels.searchPlaceholder"/>
           </div>
           <div class="field">
             <label><translate>Ordering</translate></label>
@@ -98,6 +98,16 @@ export default {
   mounted () {
     $('.ui.dropdown').dropdown()
   },
+  computed: {
+    labels () {
+      let playlists = this.$gettext('Playlists')
+      let searchPlaceholder = this.$gettext('Enter an playlist name...')
+      return {
+        playlists,
+        searchPlaceholder
+      }
+    }
+  },
   methods: {
     updateQueryString: _.debounce(function () {
       this.$router.replace({
diff --git a/front/src/views/radios/Detail.vue b/front/src/views/radios/Detail.vue
index 0975398b..e8233735 100644
--- a/front/src/views/radios/Detail.vue
+++ b/front/src/views/radios/Detail.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="'Radio'">
+    <div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <div v-if="!isLoading && radio" class="ui head vertical center aligned stripe segment" v-title="radio.name">
@@ -24,15 +24,15 @@
           </router-link>
           <dangerous-button class="labeled icon" :action="deleteRadio">
             <i class="trash icon"></i> Delete
-            <p slot="modal-header">Do you want to delete the radio "{{ radio.name }}"?</p>
-            <p slot="modal-content">This will completely delete this radio and cannot be undone.</p>
-            <p slot="modal-confirm">Delete radio</p>
+            <p slot="modal-header"><translate :translate-params="{radio: radio.name}">Do you want to delete the radio "{{ radio }}"?</translate></p>
+            <p slot="modal-content"><translate>This will completely delete this radio and cannot be undone.</translate></p>
+            <p slot="modal-confirm"><translate>Delete radio</translate></p>
           </dangerous-button>
         </template>
       </div>
     </div>
     <div class="ui vertical stripe segment">
-      <h2>Tracks</h2>
+      <h2><translate>Tracks</translate></h2>
       <track-table :tracks="tracks"></track-table>
       <div class="ui center aligned basic segment">
         <pagination
@@ -74,6 +74,13 @@ export default {
   created: function () {
     this.fetch()
   },
+  computed: {
+    labels () {
+      return {
+        title: this.$gettext('Radio')
+      }
+    }
+  },
   methods: {
     selectPage: function (page) {
       this.page = page
-- 
GitLab