From 06d7ce82fb4d431ecffcf6d9a18476a552a3d65a Mon Sep 17 00:00:00 2001
From: jovuit <jvuitton@disroot.org>
Date: Thu, 14 Feb 2019 10:26:35 +0100
Subject: [PATCH] #662: Resolve "Add contexts to translatable strings"

---
 CONTRIBUTING.rst                              |  9 ++--
 .../components/manage/library/FilesTable.vue  | 40 ++++++++--------
 .../manage/moderation/AccountsTable.vue       | 32 ++++++-------
 .../manage/moderation/DomainsTable.vue        | 28 +++++------
 .../manage/moderation/InstancePolicyCard.vue  | 18 +++----
 .../manage/moderation/InstancePolicyForm.vue  | 48 +++++++++----------
 .../manage/users/InvitationForm.vue           | 14 +++---
 .../manage/users/InvitationsTable.vue         | 30 ++++++------
 .../components/manage/users/UsersTable.vue    | 48 +++++++++----------
 9 files changed, 135 insertions(+), 132 deletions(-)

diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 05254b76d2..63cc2d771f 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -431,10 +431,10 @@ This hierarchical structure is made of several parts:
 - The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed:
     - ``Content``
     - ``Footer``
+    - ``Head``
     - ``Menu``
     - ``Popup``
     - ``Sidebar``
-    - ``Head``
     - ``*`` for strings that are not tied to a specific location
 
 - The feature part, which is required, and refers to the feature associated with the translated string:
@@ -462,11 +462,13 @@ This hierarchical structure is made of several parts:
 - The component part, which is required and refers to the type of element that contain the string:
     - ``Button``
     - ``Card``
+    - ``Checkbox``
     - ``Dropdown``
     - ``Error message``
     - ``Form``
     - ``Header``
     - ``Help text``
+    - ``Hidden text``
     - ``Icon``
     - ``Input``
     - ``Image``
@@ -481,15 +483,15 @@ This hierarchical structure is made of several parts:
     - ``Table``
     - ``Title``
     - ``Tooltip``
-    - ``Hidden text``
     - ``*`` for strings that are not tied to a specific component
 
 The detail part, which is optional and refers to the contents of the string itself, such as:
+    - ``Adjective``
     - ``Call to action``
-    - ``Verb``
     - ``Noun``
     - ``Short``
     - ``Unit``
+    - ``Verb``
 
 Here are a few examples of valid context hierarchies:
 
@@ -498,6 +500,7 @@ Here are a few examples of valid context hierarchies:
 - ``Footer/*/Help text``
 - ``*/*/*/Verb, Short``
 - ``Popup/Playlist/Button``
+- ``Content/Admin/Table.Label/Short, Noun (Value is a date)``
 
 It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot:
 
diff --git a/front/src/components/manage/library/FilesTable.vue b/front/src/components/manage/library/FilesTable.vue
index 23f33d0f35..4716e361ac 100644
--- a/front/src/components/manage/library/FilesTable.vue
+++ b/front/src/components/manage/library/FilesTable.vue
@@ -3,11 +3,11 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui field">
-          <label><translate>Search</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label'">Search</translate></label>
           <input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
-          <label><translate>Ordering</translate></label>
+          <label><translate :translate-context="'Content/Search/Dropdown.Label'">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ sharedLabels.filters[option[1]] }}
@@ -15,10 +15,10 @@
           </select>
         </div>
         <div class="field">
-          <label><translate>Order</translate></label>
+          <label><translate :translate-context="'Content/Search/Dropdown.Label/Noun'">Order</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+"><translate>Ascending</translate></option>
-            <option value="-"><translate>Descending</translate></option>
+            <option value="+"><translate :translate-context="'Content/Search/Dropdown'">Ascending</translate></option>
+            <option value="-"><translate :translate-context="'Content/Search/Dropdown'">Descending</translate></option>
           </select>
         </div>
       </div>
@@ -35,14 +35,14 @@
         :action-url="'manage/library/uploads/action/'"
         :filters="actionFilters">
         <template slot="header-cells">
-          <th><translate>Title</translate></th>
-          <th><translate>Artist</translate></th>
-          <th><translate>Album</translate></th>
-          <th><translate>Import date</translate></th>
-          <th><translate>Type</translate></th>
-          <th><translate>Bitrate</translate></th>
-          <th><translate>Duration</translate></th>
-          <th><translate>Size</translate></th>
+          <th><translate :translate-context="'*/*/*/Short, Noun'">Title</translate></th>
+          <th><translate :translate-context="'*/*/*/Short, Noun'">Artist</translate></th>
+          <th><translate :translate-context="'*/*/*/Short, Noun'">Album</translate></th>
+          <th><translate :translate-context="'Content/Library/Table.Label/Short, Noun'">Import date</translate></th>
+          <th><translate :translate-context="'Content/Library/Table.Label/Short, Noun'">Type</translate></th>
+          <th><translate :translate-context="'Content/*/*/Short, Noun'">Bitrate</translate></th>
+          <th><translate :translate-context="'Content/*/*/Short, Noun'">Duration</translate></th>
+          <th><translate :translate-context="'Content/*/*/Short, Noun'">Size</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
@@ -61,25 +61,25 @@
             {{ scope.obj.mimetype }}
           </td>
           <td v-else>
-            <translate>N/A</translate>
+            <translate :translate-context="'*/*/*'">N/A</translate>
           </td>
           <td v-if="scope.obj.bitrate">
             {{ scope.obj.bitrate | humanSize }}/s
           </td>
           <td v-else>
-            <translate>N/A</translate>
+            <translate :translate-context="'*/*/*'">N/A</translate>
           </td>
           <td v-if="scope.obj.duration">
             {{ time.parse(scope.obj.duration) }}
           </td>
           <td v-else>
-            <translate>N/A</translate>
+            <translate :translate-context="'*/*/*'">N/A</translate>
           </td>
           <td v-if="scope.obj.size">
             {{ scope.obj.size | humanSize }}
           </td>
           <td v-else>
-            <translate>N/A</translate>
+            <translate :translate-context="'*/*/*'">N/A</translate>
           </td>
         </template>
       </action-table>
@@ -95,7 +95,7 @@
         ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate
+        <translate :translate-context="'Content/Library/Paragraph'"
           :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
           Showing results %{ start }-%{ end } on %{ total }
         </translate>
@@ -173,7 +173,7 @@ export default {
   computed: {
     labels () {
       return {
-        searchPlaceholder: this.$gettext('Search by title, artist, domain…')
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by title, artist, domain…')
       }
     },
     actionFilters () {
@@ -187,7 +187,7 @@ export default {
       }
     },
     actions () {
-      let msg = this.$gettext('Delete')
+      let msg = this.$pgettext('Content/Library/Dropdown/Verb', 'Delete')
       return [
         {
           name: 'delete',
diff --git a/front/src/components/manage/moderation/AccountsTable.vue b/front/src/components/manage/moderation/AccountsTable.vue
index 8259e8ec8b..d8897ab3b7 100644
--- a/front/src/components/manage/moderation/AccountsTable.vue
+++ b/front/src/components/manage/moderation/AccountsTable.vue
@@ -3,13 +3,13 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui six wide field">
-          <label><translate>Search</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Search</translate></label>
           <form @submit.prevent="search.query = $refs.search.value">
             <input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
           </form>
         </div>
         <div class="field">
-          <label><translate>Ordering</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ sharedLabels.filters[option[1]] }}
@@ -17,10 +17,10 @@
           </select>
         </div>
         <div class="field">
-          <label><translate>Ordering direction</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering direction</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+"><translate>Ascending</translate></option>
-            <option value="-"><translate>Descending</translate></option>
+            <option value="+"><translate :translate-context="'Content/Search/Dropdown'">Ascending</translate></option>
+            <option value="-"><translate :translate-context="'Content/Search/Dropdown'">Descending</translate></option>
           </select>
         </div>
       </div>
@@ -37,12 +37,12 @@
         action-url="manage/accounts/action/"
         :filters="actionFilters">
         <template slot="header-cells">
-          <th><translate>Name</translate></th>
-          <th><translate>Domain</translate></th>
-          <th><translate>Uploads</translate></th>
-          <th><translate>First seen</translate></th>
-          <th><translate>Last seen</translate></th>
-          <th><translate>Under moderation rule</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label'">Name</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label'">Domain</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Noun'">Uploads</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Noun'">First seen</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Noun'">Last seen</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Short, Noun'">Under moderation rule</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
@@ -57,7 +57,7 @@
             </template>
             <span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.domain)">
               <i class="home icon"></i>
-              <translate>Local account</translate>
+              <translate :translate-context="'Content/Moderation/Table/Short, Noun'">Local account</translate>
             </span>
           </td>
           <td>
@@ -70,7 +70,7 @@
             <human-date v-if="scope.obj.last_fetch_date" :date="scope.obj.last_fetch_date"></human-date>
           </td>
           <td>
-            <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate>Yes</translate></span>
+            <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate :translate-context="'*/*/*'">Yes</translate></span>
           </td>
         </template>
       </action-table>
@@ -86,7 +86,7 @@
         ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate
+        <translate :translate-context="'Content/Moderation/Paragraph'"
           :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
           Showing results %{ start }-%{ end } on %{ total }
         </translate>
@@ -168,7 +168,7 @@ export default {
   computed: {
     labels () {
       return {
-        searchPlaceholder: this.$gettext('Search by domain, username, bio…')
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, username, bio…')
       }
     },
     actionFilters () {
@@ -185,7 +185,7 @@ export default {
       return [
         {
           name: 'purge',
-          label: this.$gettext('Purge'),
+          label: this.$pgettext('Content/Moderation/Dropdown/Verb', 'Purge'),
           isDangerous: true
         }
       ]
diff --git a/front/src/components/manage/moderation/DomainsTable.vue b/front/src/components/manage/moderation/DomainsTable.vue
index ed9eb5be80..f07a1e426c 100644
--- a/front/src/components/manage/moderation/DomainsTable.vue
+++ b/front/src/components/manage/moderation/DomainsTable.vue
@@ -3,11 +3,11 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui field">
-          <label><translate>Search</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Verb'">Search</translate></label>
           <input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
-          <label><translate>Ordering</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ sharedLabels.filters[option[1]] }}
@@ -15,10 +15,10 @@
           </select>
         </div>
         <div class="field">
-          <label><translate>Ordering direction</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering direction</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+"><translate>Ascending</translate></option>
-            <option value="-"><translate>Descending</translate></option>
+            <option value="+"><translate :translate-context="'Content/Search/Dropdown'">Ascending</translate></option>
+            <option value="-"><translate :translate-context="'Content/Search/Dropdown'">Descending</translate></option>
           </select>
         </div>
       </div>
@@ -36,11 +36,11 @@
         idField="name"
         :filters="actionFilters">
         <template slot="header-cells">
-          <th><translate>Name</translate></th>
-          <th><translate>Users</translate></th>
-          <th><translate>Received messages</translate></th>
-          <th><translate>First seen</translate></th>
-          <th><translate>Under moderation rule</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label'">Name</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label'">Users</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Short, Noun'">Received messages</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Short, Noun'">First seen</translate></th>
+          <th><translate :translate-context="'Content/Moderation/Table.Label/Short'">Under moderation rule</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
@@ -56,7 +56,7 @@
             <human-date :date="scope.obj.creation_date"></human-date>
           </td>
           <td>
-            <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate>Yes</translate></span>
+            <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate :translate-context="'*/*/*'">Yes</translate></span>
           </td>
         </template>
       </action-table>
@@ -72,7 +72,7 @@
         ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate
+        <translate :translate-context="'Content/Moderation/Paragraph'"
           :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
           Showing results %{ start }-%{ end } on %{ total }
         </translate>
@@ -148,7 +148,7 @@ export default {
   computed: {
     labels () {
       return {
-        searchPlaceholder: this.$gettext('Search by name…')
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name…')
       }
     },
     actionFilters () {
@@ -165,7 +165,7 @@ export default {
       return [
         {
           name: 'purge',
-          label: this.$gettext('Purge'),
+          label: this.$pgettext('Content/Moderation/Dropdown/Verb', 'Purge'),
           isDangerous: true
         }
       ]
diff --git a/front/src/components/manage/moderation/InstancePolicyCard.vue b/front/src/components/manage/moderation/InstancePolicyCard.vue
index b72c6bac8d..f88f0487ef 100644
--- a/front/src/components/manage/moderation/InstancePolicyCard.vue
+++ b/front/src/components/manage/moderation/InstancePolicyCard.vue
@@ -6,44 +6,44 @@
       <i class="user icon"></i>{{ object.actor }}  &nbsp;
       <template v-if="object.is_active">
         <i class="play icon"></i>
-        <translate>Enabled</translate>
+        <translate :translate-context="'*/*/*'">Enabled</translate>
       </template>
       <template v-if="!object.is_active">
         <i class="pause icon"></i>
-        <translate>Paused</translate>
+        <translate :translate-context="'Content/Moderation/Card.List item'">Paused</translate>
       </template>
     </p>
     <div>
-      <p><strong><translate>Rule</translate></strong></p>
+      <p><strong><translate :translate-context="'Content/Moderation/Card.Title/Noun'">Rule</translate></strong></p>
       <p v-if="object.block_all">
         <i class="ban icon"></i>
-        <translate>Block everything</translate>
+        <translate :translate-context="'Content/Moderation/Card.List item/Verb'">Block everything</translate>
       </p>
       <div v-else class="ui list">
         <div class="ui item" v-if="object.silence_activity">
           <i class="feed icon"></i>
-          <div class="content"><translate>Mute activity</translate></div>
+          <div class="content"><translate :translate-context="'Content/Moderation/Card.List item/Verb'">Mute activity</translate></div>
         </div>
         <div class="ui item" v-if="object.silence_notifications">
           <i class="bell icon"></i>
-          <div class="content"><translate>Mute notifications</translate></div>
+          <div class="content"><translate :translate-context="'Content/Moderation/Card.List item/Verb'">Mute notifications</translate></div>
         </div>
         <div class="ui item" v-if="object.reject_media">
           <i class="file icon"></i>
-          <div class="content"><translate>Reject media</translate></div>
+          <div class="content"><translate :translate-context="'Content/Moderation/Card.List item/Verb'">Reject media</translate></div>
         </div>
 
       </div>
     </div>
     <div v-if="markdown && object.summary">
       <div class="ui hidden divider"></div>
-      <p><strong><translate>Reason</translate></strong></p>
+      <p><strong><translate :translate-context="'Content/Moderation/Card.Title/Noun'">Reason</translate></strong></p>
       <div v-html="markdown.makeHtml(object.summary)"></div>
     </div>
     <div class="ui hidden divider"></div>
     <button @click="$emit('update')" class="ui right floated labeled icon button">
       <i class="edit icon"></i>
-      <translate>Update</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'">Edit</translate>
     </button>
   </div>
 </template>
diff --git a/front/src/components/manage/moderation/InstancePolicyForm.vue b/front/src/components/manage/moderation/InstancePolicyForm.vue
index e90f9ad10d..05ccecc53c 100644
--- a/front/src/components/manage/moderation/InstancePolicyForm.vue
+++ b/front/src/components/manage/moderation/InstancePolicyForm.vue
@@ -1,11 +1,11 @@
 <template>
   <form class="ui form" @submit.prevent="createOrUpdate">
     <h3 class="ui header">
-      <translate v-if="object" key="1">Update moderation rule</translate>
-      <translate v-else key="2">Add a new moderation rule</translate>
+      <translate :translate-context="'Content/Moderation/Card.Title/Verb'" v-if="object" key="1">Edit moderation rule</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'" v-else key="2">Add a new moderation rule</translate>
     </h3>
     <div v-if="errors && errors.length > 0" class="ui negative message">
-      <div class="header"><translate>Error while creating rule</translate></div>
+      <div class="header"><translate :translate-context="'Content/Moderation/Error message.Title'">Error while creating rule</translate></div>
       <ul class="list">
         <li v-for="error in errors">{{ error }}</li>
       </ul>
@@ -15,15 +15,15 @@
       <div class="ui toggle checkbox">
         <input id="policy-is-active" v-model="current.isActive" type="checkbox">
         <label for="policy-is-active">
-          <translate v-if="current.isActive" key="1">Enabled</translate>
-          <translate v-else key="2">Disabled</translate>
+          <translate :translate-context="'*/*/*'" v-if="current.isActive" key="1">Enabled</translate>
+          <translate :translate-context="'*/*/*'" v-else key="2">Disabled</translate>
           <tooltip :content="labels.isActiveHelp" />
         </label>
       </div>
     </div>
     <div class="field">
       <label for="policy-summary">
-        <translate>Reason</translate>
+        <translate :translate-context="'Content/Moderation/Input.Label/Noun'">Reason</translate>
         <tooltip :content="labels.summaryHelp" />
       </label>
       <textarea name="policy-summary" id="policy-summary" rows="5" v-model="current.summary"></textarea>
@@ -32,13 +32,13 @@
       <div class="ui toggle checkbox">
         <input id="policy-is-active" v-model="current.blockAll" type="checkbox">
         <label for="policy-is-active">
-          <translate>Block everything</translate>
+          <translate :translate-context="'Content/Moderation/Checkbox.Label/Verb'">Block everything</translate>
           <tooltip :content="labels.blockAllHelp" />
         </label>
       </div>
     </div>
     <div class="ui horizontal divider">
-      <translate>Or customize your rule</translate>
+      <translate :translate-context="'Content/Moderation/Card.Title'">Or customize your rule</translate>
     </div>
     <div v-for="config in fieldConfig" :class="['field']">
       <div class="ui toggle checkbox">
@@ -52,22 +52,22 @@
     </div>
     <div class="ui hidden divider"></div>
     <button @click="$emit('cancel')" class="ui basic left floated button">
-      <translate>Cancel</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'">Cancel</translate>
     </button>
     <button :class="['ui', 'right', 'floated', 'green', {'disabled loading': isLoading}, 'button']" :disabled="isLoading">
-      <translate v-if="object" key="1">Update</translate>
-      <translate v-else key="2">Create</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'" v-if="object" key="1">Update</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'" v-else key="2">Create</translate>
     </button>
     <dangerous-button v-if="object" class="right floated basic button" color='red' @confirm="remove">
-      <translate>Delete</translate>
+      <translate :translate-context="'Content/Moderation/Card.Button.Label/Verb'">Delete</translate>
       <p slot="modal-header">
-        <translate>Delete this moderation rule?</translate>
+        <translate :translate-context="'Popup/Moderation/Title'">Delete this moderation rule?</translate>
       </p>
       <p slot="modal-content">
-        <translate>This action is irreversible.</translate>
+        <translate :translate-context="'Popup/Moderation/Paragraph'">This action is irreversible.</translate>
       </p>
       <p slot="modal-confirm">
-        <translate>Delete moderation rule</translate>
+        <translate :translate-context="'Popup/Moderation/Button.Label/Verb'">Delete moderation rule</translate>
       </p>
     </dangerous-button>
   </form>
@@ -107,20 +107,20 @@ export default {
   computed: {
     labels () {
       return {
-        summaryHelp: this.$gettext("Explain why you're applying this policy. Depending on your instance configuration, this will help you remember why you acted on this account or domain, and may be displayed publicly to help users understand what moderation rules are in place."),
-        isActiveHelp: this.$gettext("Use this setting to temporarily enable/disable the policy without completely removing it."),
-        blockAllHelp: this.$gettext("Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)"),
+        summaryHelp: this.$pgettext('Content/Moderation/Help text', "Explain why you're applying this policy. Depending on your instance configuration, this will help you remember why you acted on this account or domain, and may be displayed publicly to help users understand what moderation rules are in place."),
+        isActiveHelp: this.$pgettext('Content/Moderation/Help text', "Use this setting to temporarily enable/disable the policy without completely removing it."),
+        blockAllHelp: this.$pgettext('Content/Moderation/Help text', "Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)"),
         silenceActivity: {
-          help: this.$gettext("Hide account or domain content, except from followers."),
-          label: this.$gettext("Mute activity"),
+          help: this.$pgettext('Content/Moderation/Help text', "Hide account or domain content, except from followers."),
+          label: this.$pgettext('Content/Moderation/Checkbox.Label/Verb', "Mute activity"),
         },
         silenceNotifications: {
-          help: this.$gettext("Prevent account or domain from triggering notifications, except from followers."),
-          label: this.$gettext("Silence notifications"),
+          help: this.$pgettext('Content/Moderation/Help text', "Prevent account or domain from triggering notifications, except from followers."),
+          label: this.$pgettext('Content/Moderation/Checkbox.Label/Verb', "Mute notifications"),
         },
         rejectMedia: {
-          help: this.$gettext("Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well."),
-          label: this.$gettext("Reject media"),
+          help: this.$pgettext('Content/Moderation/Help text', "Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well."),
+          label: this.$pgettext('Content/Moderation/Checkbox.Label/Verb', "Reject media"),
         }
       }
     }
diff --git a/front/src/components/manage/users/InvitationForm.vue b/front/src/components/manage/users/InvitationForm.vue
index 02cfba5a94..32f1842735 100644
--- a/front/src/components/manage/users/InvitationForm.vue
+++ b/front/src/components/manage/users/InvitationForm.vue
@@ -2,19 +2,19 @@
   <div>
     <form class="ui form" @submit.prevent="submit">
       <div v-if="errors.length > 0" class="ui negative message">
-        <div class="header"><translate>Error while creating invitation</translate></div>
+        <div class="header"><translate :translate-context="'Content/Admin/Error message.Title'">Error while creating invitation</translate></div>
         <ul class="list">
           <li v-for="error in errors">{{ error }}</li>
         </ul>
       </div>
       <div class="inline fields">
         <div class="ui field">
-          <label><translate>Invitation code</translate></label>
+          <label><translate :translate-context="'Content/Admin/Input.Label/Noun'">Invitation code</translate></label>
           <input name="code" type="text" v-model="code" :placeholder="labels.placeholder" />
         </div>
         <div class="ui field">
           <button :class="['ui', {loading: isLoading}, 'button']" :disabled="isLoading" type="submit">
-            <translate>Get a new invitation</translate>
+            <translate :translate-context="'Content/Admin/Button.Label/Verb'">Get a new invitation</translate>
           </button>
         </div>
       </div>
@@ -24,8 +24,8 @@
       <table class="ui ui basic table">
         <thead>
           <tr>
-            <th><translate>Code</translate></th>
-            <th><translate>Share link</translate></th>
+            <th><translate :translate-context="'Content/Admin/Table.Label/Noun'">Code</translate></th>
+            <th><translate :translate-context="'Content/Admin/Table.Label/Noun'">Share link</translate></th>
           </tr>
         </thead>
         <tbody>
@@ -35,7 +35,7 @@
           </tr>
         </tbody>
       </table>
-      <button class="ui basic button" @click="invitations = []"><translate>Clear</translate></button>
+      <button class="ui basic button" @click="invitations = []"><translate :translate-context="'Content/Admin/Button.Label/Verb'">Clear</translate></button>
     </div>
   </div>
 </template>
@@ -55,7 +55,7 @@ export default {
   computed: {
     labels () {
       return {
-        placeholder: this.$gettext('Leave empty for a random code')
+        placeholder: this.$pgettext('Content/Admin/Input.Placeholder', 'Leave empty for a random code')
       }
     }
   },
diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue
index ee9eb50350..38cf15b77d 100644
--- a/front/src/components/manage/users/InvitationsTable.vue
+++ b/front/src/components/manage/users/InvitationsTable.vue
@@ -3,11 +3,11 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui field">
-          <label><translate>Search</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Verb'">Search</translate></label>
           <input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
-          <label><translate>Ordering</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ sharedLabels.filters[option[1]] }}
@@ -15,11 +15,11 @@
           </select>
         </div>
         <div class="field">
-          <label><translate>Status</translate></label>
+          <label><translate :translate-context="'Content/Admin/Dropdown.Label'">Status</translate></label>
           <select class="ui dropdown" v-model="isOpen">
-            <option :value="null"><translate>All</translate></option>
-            <option :value="true"><translate>Open</translate></option>
-            <option :value="false"><translate>Expired/used</translate></option>
+            <option :value="null"><translate :translate-context="'Content/Admin/Dropdown'">All</translate></option>
+            <option :value="true"><translate :translate-context="'Content/Admin/Dropdown/Adjective'">Open</translate></option>
+            <option :value="false"><translate :translate-context="'Content/Admin/Dropdown/Adjective'">Expired/used</translate></option>
           </select>
         </div>
       </div>
@@ -36,20 +36,20 @@
         :action-url="'manage/users/invitations/action/'"
         :filters="actionFilters">
         <template slot="header-cells">
-          <th><translate>Owner</translate></th>
-          <th><translate>Status</translate></th>
-          <th><translate>Creation date</translate></th>
-          <th><translate>Expiration date</translate></th>
-          <th><translate>Code</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Owner</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Status</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Creation date</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Expiration date</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Code</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
             <router-link :to="{name: 'manage.users.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.owner.username }}</router-link>
           </td>
           <td>
-            <span v-if="scope.obj.users.length > 0" class="ui green basic label"><translate>Used</translate></span>
-            <span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui red basic label"><translate>Expired</translate></span>
-            <span v-else class="ui basic label"><translate>Not used</translate></span>
+            <span v-if="scope.obj.users.length > 0" class="ui green basic label"><translate :translate-context="'Content/Admin/Table'">Used</translate></span>
+            <span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui red basic label"><translate :translate-context="'Content/Admin/Table'">Expired</translate></span>
+            <span v-else class="ui basic label"><translate :translate-context="'Content/Admin/Table'">Not used</translate></span>
           </td>
           <td>
             <human-date :date="scope.obj.creation_date"></human-date>
@@ -74,7 +74,7 @@
         ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate
+        <translate :translate-context="'Content/Admin/Paragraph'"
           :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
           Showing results %{ start }-%{ end } on %{ total }
         </translate>
diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue
index d76634a331..96015faf60 100644
--- a/front/src/components/manage/users/UsersTable.vue
+++ b/front/src/components/manage/users/UsersTable.vue
@@ -3,11 +3,11 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui field">
-          <label><translate>Search</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/verb'">Search</translate></label>
           <input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
         </div>
         <div class="field">
-          <label><translate>Ordering</translate></label>
+          <label><translate :translate-context="'Content/Search/Input.Label/Noun'">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
             <option v-for="option in orderingOptions" :value="option[0]">
               {{ sharedLabels.filters[option[1]] }}
@@ -15,10 +15,10 @@
           </select>
         </div>
         <div class="field">
-          <label><translate>Order</translate></label>
+          <label><translate :translate-context="'Content/Search/Dropdown.Label/Noun'">Order</translate></label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+"><translate>Ascending</translate></option>
-            <option value="-"><translate>Descending</translate></option>
+            <option value="+"><translate :translate-context="'Content/Search/Dropdown'">Ascending</translate></option>
+            <option value="-"><translate :translate-context="'Content/Search/Dropdown'">Descending</translate></option>
           </select>
         </div>
       </div>
@@ -35,13 +35,13 @@
         :action-url="'manage/library/uploads/action/'"
         :filters="actionFilters">
         <template slot="header-cells">
-          <th><translate>Username</translate></th>
-          <th><translate>Email</translate></th>
-          <th><translate>Account status</translate></th>
-          <th><translate>Sign-up</translate></th>
-          <th><translate>Last activity</translate></th>
-          <th><translate>Permissions</translate></th>
-          <th><translate>Status</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Username</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label'">Email</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label/Short, Noun'">Account status</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label/Short, Noun (Value is a date)'">Sign-up</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label/Short, Noun (Value is a date)'">Last activity</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label/Noun'">Permissions</translate></th>
+          <th><translate :translate-context="'Content/Admin/Table.Label/Noun'">Status</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
@@ -51,15 +51,15 @@
             <span>{{ scope.obj.email }}</span>
           </td>
           <td>
-            <span v-if="scope.obj.is_active" class="ui basic green label"><translate>Active</translate></span>
-            <span v-else class="ui basic grey label"><translate>Inactive</translate></span>
+            <span v-if="scope.obj.is_active" class="ui basic green label"><translate :translate-context="'Content/Admin/Table'">Active</translate></span>
+            <span v-else class="ui basic grey label"><translate :translate-context="'Content/Admin/Table'">Inactive</translate></span>
           </td>
           <td>
             <human-date :date="scope.obj.date_joined"></human-date>
           </td>
           <td>
             <human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date>
-            <template v-else><translate>N/A</translate></template>
+            <template v-else><translate :translate-context="'*/*/*'">N/A</translate></template>
           </td>
           <td>
             <template v-for="p in permissions">
@@ -67,9 +67,9 @@
             </template>
           </td>
           <td>
-            <span v-if="scope.obj.is_superuser" class="ui pink label"><translate>Admin</translate></span>
-            <span v-else-if="scope.obj.is_staff" class="ui purple label"><translate>Staff member</translate></span>
-            <span v-else class="ui basic label"><translate>regular user</translate></span>
+            <span v-if="scope.obj.is_superuser" class="ui pink label"><translate :translate-context="'Content/Admin/Table.User role'">Admin</translate></span>
+            <span v-else-if="scope.obj.is_staff" class="ui purple label"><translate :translate-context="'Content/Admin/Table.User role'">Staff member</translate></span>
+            <span v-else class="ui basic label"><translate :translate-context="'Content/Admin/Table, User role'">regular user</translate></span>
           </td>
         </template>
       </action-table>
@@ -85,7 +85,7 @@
         ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate
+        <translate :translate-context="'Content/Admin/Paragraph'"
           :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
           Showing results %{ start }-%{ end } on %{ total }
         </translate>
@@ -160,7 +160,7 @@ export default {
   computed: {
     labels () {
       return {
-        searchPlaceholder: this.$gettext('Search by username, e-mail address, name…')
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by username, e-mail address, name…')
       }
     },
     privacyLevels () {
@@ -170,15 +170,15 @@ export default {
       return [
         {
           'code': 'library',
-          'label': this.$gettext('Library')
+          'label': this.$pgettext('Content/Admin/Table', 'Library')
         },
         {
           'code': 'moderation',
-          'label': this.$gettext('Moderation')
+          'label': this.$pgettext('Content/Admin/Table', 'Moderation')
         },
         {
           'code': 'settings',
-          'label': this.$gettext('Settings')
+          'label': this.$pgettext('Content/Admin/Table', 'Settings')
         }
       ]
     },
@@ -196,7 +196,7 @@ export default {
       return [
         // {
         //   name: 'delete',
-        //   label: this.$gettext('Delete'),
+        //   label: this.$pgettext('Content/Admin/Button.Label/Verb', 'Delete'),
         //   isDangerous: true
         // }
       ]
-- 
GitLab