From 7c4a76cc89824544d45135def033d3d651bea345 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Sun, 7 Nov 2021 21:02:06 +0100
Subject: [PATCH] More bidi improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 config/dev.exs                                |  2 +-
 js/src/common.scss                            | 33 +++++++++++
 .../components/About/InstanceContactLink.vue  |  4 +-
 js/src/components/Account/ActorCard.vue       |  2 +-
 js/src/components/Comment/Comment.vue         |  4 +-
 js/src/components/Event/EventMetadataList.vue |  1 +
 .../components/Event/EventMinimalistCard.vue  |  1 +
 .../Event/EventParticipationCard.vue          |  4 +-
 .../Event/FullAddressAutoComplete.vue         |  1 +
 js/src/components/Event/OrganizerPicker.vue   |  3 +-
 .../Event/OrganizerPickerWrapper.vue          |  2 +
 .../components/Event/ParticipationButton.vue  |  4 +-
 js/src/components/Event/TagInput.vue          |  1 +
 js/src/components/Post/PostListItem.vue       |  3 +-
 js/src/components/SearchField.vue             |  1 +
 js/src/i18n/ar.json                           |  2 +-
 js/src/i18n/be.json                           |  2 +-
 js/src/i18n/ca.json                           |  3 +-
 js/src/i18n/cs.json                           |  2 +-
 js/src/i18n/de.json                           |  3 +-
 js/src/i18n/en_US.json                        |  3 +-
 js/src/i18n/es.json                           |  5 +-
 js/src/i18n/fa.json                           |  2 +-
 js/src/i18n/fi.json                           |  3 +-
 js/src/i18n/fr_FR.json                        |  5 +-
 js/src/i18n/gd.json                           |  3 +-
 js/src/i18n/gl.json                           |  3 +-
 js/src/i18n/hu.json                           |  3 +-
 js/src/i18n/id.json                           |  3 +-
 js/src/i18n/it.json                           |  3 +-
 js/src/i18n/nl.json                           |  2 +-
 js/src/i18n/nn.json                           |  5 +-
 js/src/i18n/oc.json                           |  3 +-
 js/src/i18n/pl.json                           |  3 +-
 js/src/i18n/pt.json                           |  2 +-
 js/src/i18n/pt_BR.json                        |  3 +-
 js/src/i18n/ru.json                           |  3 +-
 js/src/i18n/sl.json                           |  3 +-
 js/src/i18n/sv.json                           |  4 +-
 js/src/utils/i18n.ts                          |  6 ++
 js/src/views/About/AboutInstance.vue          |  4 +-
 js/src/views/Event/Edit.vue                   |  1 +
 js/src/views/Event/Event.vue                  | 57 ++++---------------
 js/src/views/Group/Group.vue                  |  8 ++-
 js/src/views/Posts/Edit.vue                   |  1 +
 js/src/views/Search.vue                       |  1 +
 .../__snapshots__/PostListItem.spec.ts.snap   | 12 ++--
 lib/web/views/utils.ex                        | 11 +++-
 48 files changed, 127 insertions(+), 113 deletions(-)

diff --git a/config/dev.exs b/config/dev.exs
index 291bdc463..cd4d9ea1c 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -58,7 +58,7 @@ config :logger, :console, format: "[$level] $message\n", level: :debug
 
 config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
 
-config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en"]
+config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en", "ar"]
 
 # Set a higher stacktrace during development. Avoid configuring such
 # in production as building large stacktraces may be expensive.
diff --git a/js/src/common.scss b/js/src/common.scss
index 9566e8925..65301b01b 100644
--- a/js/src/common.scss
+++ b/js/src/common.scss
@@ -165,3 +165,36 @@ p {
 .icon {
   vertical-align: middle;
 }
+
+.tags .tag:not(:last-child) {
+  margin-right: unset;
+  @include margin-right(0.5rem);
+}
+
+.button .icon {
+  &:first-child:not(:last-child) {
+    @include margin-left(calc(-0.5em - 1px));
+    @include margin-right(0.25em);
+  }
+  &:last-child:not(:first-child) {
+    @include margin-right(calc(-0.5em - 1px));
+    @include margin-left(0.25em);
+  }
+}
+
+.buttons .button:not(:last-child):not(.is-fullwidth) {
+  margin-right: unset;
+  @include margin-right(0.5rem);
+}
+
+.breadcrumb li:first-child a {
+  padding-left: unset;
+  @include padding-left(0);
+  @include padding-right(0.75em);
+}
+.media-left {
+  @include margin-left(1rem);
+}
+a.dropdown-item {
+  @include padding-right(3rem);
+}
diff --git a/js/src/components/About/InstanceContactLink.vue b/js/src/components/About/InstanceContactLink.vue
index 80d17ecea..429e7fb67 100644
--- a/js/src/components/About/InstanceContactLink.vue
+++ b/js/src/components/About/InstanceContactLink.vue
@@ -1,9 +1,9 @@
 <template>
   <p>
-    <a :title="contact" v-if="configLink" :href="configLink.uri">{{
+    <a dir="auto" :title="contact" v-if="configLink" :href="configLink.uri">{{
       configLink.text
     }}</a>
-    <span v-else-if="contact">{{ contact }}</span>
+    <span dir="auto" v-else-if="contact">{{ contact }}</span>
     <span v-else>{{ $t("contact uninformed") }}</span>
   </p>
 </template>
diff --git a/js/src/components/Account/ActorCard.vue b/js/src/components/Account/ActorCard.vue
index 4b7a9b11a..d319bd898 100644
--- a/js/src/components/Account/ActorCard.vue
+++ b/js/src/components/Account/ActorCard.vue
@@ -12,7 +12,7 @@
         {{ actor.name || `@${usernameWithDomain(actor)}` }}
       </p>
       <p class="has-text-grey-dark" v-if="actor.name">
-        @{{ usernameWithDomain(actor) }}
+        <span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
       </p>
       <div
         v-if="full"
diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue
index 86fa68f5a..28267be3c 100644
--- a/js/src/components/Comment/Comment.vue
+++ b/js/src/components/Comment/Comment.vue
@@ -36,7 +36,7 @@
             <strong :class="{ organizer: commentFromOrganizer }">{{
               comment.actor.name
             }}</strong>
-            <small>@{{ usernameWithDomain(comment.actor) }}</small>
+            <small dir="ltr">@{{ usernameWithDomain(comment.actor) }}</small>
           </span>
           <a v-else class="comment-link" :href="commentURL">
             <span>{{ $t("[deleted]") }}</span>
@@ -128,7 +128,7 @@
           <div class="content">
             <span class="first-line">
               <strong>{{ currentActor.name }}</strong>
-              <small>@{{ currentActor.preferredUsername }}</small>
+              <small dir="ltr">@{{ currentActor.preferredUsername }}</small>
             </span>
             <br />
             <span class="editor-line">
diff --git a/js/src/components/Event/EventMetadataList.vue b/js/src/components/Event/EventMetadataList.vue
index 9868cbedd..c43bd70cf 100644
--- a/js/src/components/Event/EventMetadataList.vue
+++ b/js/src/components/Event/EventMetadataList.vue
@@ -25,6 +25,7 @@
         :placeholder="$t('e.g. Accessibility, Twitch, PeerTube')"
         id="event-metadata-autocomplete"
         @select="(option) => addElement(option)"
+        dir="auto"
       >
         <template slot-scope="props">
           <div class="media">
diff --git a/js/src/components/Event/EventMinimalistCard.vue b/js/src/components/Event/EventMinimalistCard.vue
index 0e6a5715a..be0e47815 100644
--- a/js/src/components/Event/EventMinimalistCard.vue
+++ b/js/src/components/Event/EventMinimalistCard.vue
@@ -1,6 +1,7 @@
 <template>
   <router-link
     class="event-minimalist-card-wrapper"
+    dir="auto"
     :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
   >
     <div class="event-preview mr-0 ml-0">
diff --git a/js/src/components/Event/EventParticipationCard.vue b/js/src/components/Event/EventParticipationCard.vue
index 9b6328273..5c75a6110 100644
--- a/js/src/components/Event/EventParticipationCard.vue
+++ b/js/src/components/Event/EventParticipationCard.vue
@@ -43,8 +43,8 @@
             </router-link>
           </div>
         </div>
-        <div class="list-card-content" dir="auto">
-          <div class="title-wrapper">
+        <div class="list-card-content">
+          <div class="title-wrapper" dir="auto">
             <router-link
               :to="{
                 name: RouteName.EVENT,
diff --git a/js/src/components/Event/FullAddressAutoComplete.vue b/js/src/components/Event/FullAddressAutoComplete.vue
index 7518aa2f3..0434e944f 100644
--- a/js/src/components/Event/FullAddressAutoComplete.vue
+++ b/js/src/components/Event/FullAddressAutoComplete.vue
@@ -35,6 +35,7 @@
           v-bind="$attrs"
           :id="id"
           :disabled="disabled"
+          dir="auto"
         >
           <template #default="{ option }">
             <b-icon :icon="option.poiInfos.poiIcon.icon" />
diff --git a/js/src/components/Event/OrganizerPicker.vue b/js/src/components/Event/OrganizerPicker.vue
index 771e183ae..99e044042 100644
--- a/js/src/components/Event/OrganizerPicker.vue
+++ b/js/src/components/Event/OrganizerPicker.vue
@@ -1,6 +1,7 @@
 <template>
   <div class="list is-hoverable">
     <b-input
+      dir="auto"
       :placeholder="$t('Filter by profile or group name')"
       v-model="actorFilter"
     />
@@ -11,7 +12,7 @@
       v-for="availableActor in actualFilteredAvailableActors"
       :key="availableActor.id"
     >
-      <div class="media">
+      <div class="media" dir="auto">
         <figure class="image is-48x48" v-if="availableActor.avatar">
           <img
             class="media-left is-rounded"
diff --git a/js/src/components/Event/OrganizerPickerWrapper.vue b/js/src/components/Event/OrganizerPickerWrapper.vue
index 77b36a79e..268e266a6 100644
--- a/js/src/components/Event/OrganizerPickerWrapper.vue
+++ b/js/src/components/Event/OrganizerPickerWrapper.vue
@@ -4,6 +4,7 @@
     <div
       v-if="inline && selectedActor.id"
       class="inline box"
+      dir="auto"
       @click="isComponentModalActive = true"
     >
       <div class="media">
@@ -65,6 +66,7 @@
                 <b-input
                   :placeholder="$t('Filter by name')"
                   v-model="contactFilter"
+                  dir="auto"
                 />
                 <p
                   class="field"
diff --git a/js/src/components/Event/ParticipationButton.vue b/js/src/components/Event/ParticipationButton.vue
index 4fa3bd5c6..48fd0b93a 100644
--- a/js/src/components/Event/ParticipationButton.vue
+++ b/js/src/components/Event/ParticipationButton.vue
@@ -123,8 +123,8 @@ A button to set your participation
         @keyup.enter="joinEvent(currentActor)"
       >
         <div class="media">
-          <div class="media-left">
-            <figure class="image is-32x32" v-if="currentActor.avatar">
+          <div class="media-left" v-if="currentActor.avatar">
+            <figure class="image is-32x32">
               <img class="is-rounded" :src="currentActor.avatar.url" alt />
             </figure>
           </div>
diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue
index c484471ba..ec4606fc4 100644
--- a/js/src/components/Event/TagInput.vue
+++ b/js/src/components/Event/TagInput.vue
@@ -23,6 +23,7 @@
       :placeholder="$t('Eg: Stockholm, Dance, Chess…')"
       @typing="getFilteredTags"
       :id="id"
+      dir="auto"
     >
     </b-taginput>
   </b-field>
diff --git a/js/src/components/Post/PostListItem.vue b/js/src/components/Post/PostListItem.vue
index a2899dc5a..ac08a2e6d 100644
--- a/js/src/components/Post/PostListItem.vue
+++ b/js/src/components/Post/PostListItem.vue
@@ -1,6 +1,7 @@
 <template>
   <router-link
     class="post-minimalist-card-wrapper"
+    dir="auto"
     :to="{ name: RouteName.POST, params: { slug: post.slug } }"
   >
     <lazy-image-wrapper
@@ -12,7 +13,7 @@
       <h3 class="post-minimalist-title">{{ post.title }}</h3>
       <p class="post-publication-date">
         <b-icon icon="clock" />
-        <span class="has-text-grey-dark" v-if="isBeforeLastWeek">{{
+        <span dir="auto" class="has-text-grey-dark" v-if="isBeforeLastWeek">{{
           publishedAt | formatDateTimeString(undefined, false, "short")
         }}</span>
         <span v-else>{{
diff --git a/js/src/components/SearchField.vue b/js/src/components/SearchField.vue
index 9261059a3..c81650761 100644
--- a/js/src/components/SearchField.vue
+++ b/js/src/components/SearchField.vue
@@ -7,6 +7,7 @@
       icon="magnify"
       type="search"
       rounded
+      dir="auto"
       :placeholder="defaultPlaceHolder"
       v-model="search"
       @keyup.native.enter="enter"
diff --git a/js/src/i18n/ar.json b/js/src/i18n/ar.json
index 2f900c2ed..32eade87e 100644
--- a/js/src/i18n/ar.json
+++ b/js/src/i18n/ar.json
@@ -29,7 +29,7 @@
   "Are you sure you want to cancel your participation at event \"{title}\"?": "هل أنت متأكد مِن أنك تريد الغاء مشاركتك في فعالية \"{title}\"؟",
   "Avatar": "الصورة الرمزية",
   "Back to previous page": "العودة إلى الصفحة السابقة",
-  "By @{username}": "حسب @{username}",
+  "By {username}": "حسب {username}",
   "By {username} and {group}": "مِن {username} و {group}",
   "Cancel": "الغاء",
   "Cancel creation": "إلغاء الإنشاء",
diff --git a/js/src/i18n/be.json b/js/src/i18n/be.json
index 97c0936ac..9903ad94e 100644
--- a/js/src/i18n/be.json
+++ b/js/src/i18n/be.json
@@ -34,7 +34,7 @@
   "Are you sure you want to delete this event? This action cannot be reverted.": "Вы сапраўды хочаце выдаліць гэту падзею? Гэта дзеянне нельга адмяніць.",
   "Avatar": "Аватар",
   "Before you can login, you need to click on the link inside it to validate your account.": "Каб увайсці з уліковым запісам, патрэбна спачатку перайсці па спасылцы, якая прыйшла вам у лісце.",
-  "By @{username}": "Ад @{username}",
+  "By {username}": "Ад {username}",
   "Cancel": "Адмяніць",
   "Cancel creation": "Адмяніць стварэнне",
   "Cancel edition": "Адмяніць рэдагаванне",
diff --git a/js/src/i18n/ca.json b/js/src/i18n/ca.json
index 89d2c4117..6fc2c8c03 100644
--- a/js/src/i18n/ca.json
+++ b/js/src/i18n/ca.json
@@ -120,8 +120,7 @@
   "Begins on": "Comença a",
   "Bold": "Negreta",
   "Browser notifications": "Notificacions de navegador",
-  "By @{group}": "De @{group}",
-  "By @{username}": "De @{username}",
+  "By {username}": "De {username}",
   "By others": "Les d'altres",
   "By {author}": "De {author}",
   "By {group}": "De {group}",
diff --git a/js/src/i18n/cs.json b/js/src/i18n/cs.json
index 068bf175c..d40660a09 100644
--- a/js/src/i18n/cs.json
+++ b/js/src/i18n/cs.json
@@ -31,7 +31,7 @@
   "Avatar": "Avatar",
   "Back to previous page": "Zpět na předchozí stránku",
   "Before you can login, you need to click on the link inside it to validate your account.": "Před přihlášením musíte kliknout na odkaz v e-mailu pro potvrzení účtu.",
-  "By @{username}": "Od @{username}",
+  "By {username}": "Od {username}",
   "Cancel": "Zrušit",
   "Cancel anonymous participation": "Zrušit anonymní účast",
   "Cancel creation": "Zrušit vytváření",
diff --git a/js/src/i18n/de.json b/js/src/i18n/de.json
index ecb2d459c..68dc1b6be 100644
--- a/js/src/i18n/de.json
+++ b/js/src/i18n/de.json
@@ -118,8 +118,7 @@
   "Begins on": "Beginnt um",
   "Bold": "Fett",
   "Browser notifications": "Browserbenachrichtigungen",
-  "By @{group}": "Von @{group}",
-  "By @{username}": "von @{username}",
+  "By {username}": "von {username}",
   "By others": "Von Anderen",
   "By {author}": "Von {author}",
   "By {group}": "Von {group}",
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index 43630c71d..98c5994af 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -34,7 +34,7 @@
   "Avatar": "Avatar",
   "Back to previous page": "Back to previous page",
   "Before you can login, you need to click on the link inside it to validate your account.": "Before you can login, you need to click on the link inside it to validate your account.",
-  "By @{username}": "By @{username}",
+  "By {username}": "By {username}",
   "Cancel anonymous participation": "Cancel anonymous participation",
   "Cancel creation": "Cancel creation",
   "Cancel edition": "Cancel edition",
@@ -418,7 +418,6 @@
   "(Masked)": "(Masked)",
   "{available}/{capacity} available places": "No places left|{available}/{capacity} available places",
   "No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating",
-  "By @{group}": "By @{group}",
   "Date and time": "Date and time",
   "Location": "Location",
   "No resources selected": "No resources selected|One resources selected|{count} resources selected",
diff --git a/js/src/i18n/es.json b/js/src/i18n/es.json
index 5612c1090..98f86115e 100644
--- a/js/src/i18n/es.json
+++ b/js/src/i18n/es.json
@@ -146,12 +146,11 @@
   "Breadcrumbs": "Migajas",
   "Browser notifications": "Notificaciones del navegador",
   "Bullet list": "Lista de puntos",
-  "By @{group}": "Por @{group}",
-  "By @{username}": "Por @{username}",
+  "By {username}": "Por {username}",
   "By @{username} and @{group}": "Por @{username} y @{group}",
   "By others": "Por otros",
   "By {author}": "Por {author}",
-  "By {group}": "Por {grup}",
+  "By {group}": "Por {group}",
   "By {username} and {group}": "Por {username} y {group}",
   "Can be an email or a link, or just plain text.": "Puede ser un correo electrónico o un enlace, o simplemente texto sin formato.",
   "Cancel": "Cancelar",
diff --git a/js/src/i18n/fa.json b/js/src/i18n/fa.json
index 8b8ec5e85..a742da210 100644
--- a/js/src/i18n/fa.json
+++ b/js/src/i18n/fa.json
@@ -33,7 +33,7 @@
   "Avatar": "آواتار",
   "Back to previous page": "بازگشت به صفحه قبلی",
   "Before you can login, you need to click on the link inside it to validate your account.": "پیش از آن که بتواند وارد شوید لازم است روی پیوندی که داخل آن است کلیک کنید تا حساب‌تان اعتبارسنجی شود.",
-  "By @{username}": "توسط @{username}",
+  "By {username}": "توسط {username}",
   "Cancel": "لغو",
   "Cancel anonymous participation": "غلو حضور ناشناس",
   "Cancel creation": "لغو ساخت",
diff --git a/js/src/i18n/fi.json b/js/src/i18n/fi.json
index b44e55622..2f4ca26a4 100644
--- a/js/src/i18n/fi.json
+++ b/js/src/i18n/fi.json
@@ -132,8 +132,7 @@
   "Booking": "Varaus",
   "Breadcrumbs": "Leivänmurut",
   "Browser notifications": "Selaimen ilmoitukset",
-  "By @{group}": "Tehnyt @{group}",
-  "By @{username}": "Tehnyt @{username}",
+  "By {username}": "Tehnyt {username}",
   "By others": "Muilta",
   "By {author}": "Tekijä {author}",
   "By {group}": "Tekijä {group}",
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 48bae4f31..d1b380733 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -133,8 +133,7 @@
   "Breadcrumbs": "Fil d'Ariane",
   "Browser notifications": "Notifications du navigateur",
   "Bullet list": "Liste à puce",
-  "By @{group}": "Par @{group}",
-  "By @{username}": "Par @{username}",
+  "By {username}": "Par {username}",
   "By others": "Des autres",
   "By {author}": "Par {author}",
   "By {group}": "Par {group}",
@@ -1031,7 +1030,7 @@
   "Whether the event is accessible with a wheelchair": "Si l'événement est accessible avec un fauteuil roulant",
   "Whether the event is interpreted in sign language": "Si l'événement est interprété en langue des signes",
   "Whether the event live video is subtitled": "Si le direct vidéo de l'événement est sous-titré",
-  "Who can post a comment?": "Who can post a comment?",
+  "Who can post a comment?": "Qui peut poster un commentaire ?",
   "Who can view this event and participate": "Qui peut voir cet événement et y participer",
   "Who can view this post": "Qui peut voir ce billet",
   "Who published {number} events": "Ayant publié {number} événements",
diff --git a/js/src/i18n/gd.json b/js/src/i18n/gd.json
index 91dced484..832ae6332 100644
--- a/js/src/i18n/gd.json
+++ b/js/src/i18n/gd.json
@@ -132,8 +132,7 @@
   "Breadcrumbs": "Breadcrumbs",
   "Browser notifications": "Brathan a’ bhrabhsair",
   "Bullet list": "Liosta pheilearaichte",
-  "By @{group}": "Le @{group}",
-  "By @{username}": "Le @{username}",
+  "By {username}": "Le {username}",
   "By others": "Le daoine eile",
   "By {author}": "Le {author}",
   "By {group}": "Le {group}",
diff --git a/js/src/i18n/gl.json b/js/src/i18n/gl.json
index cd246ad1a..e790bdf7a 100644
--- a/js/src/i18n/gl.json
+++ b/js/src/i18n/gl.json
@@ -123,8 +123,7 @@
   "Bold": "Resaltado",
   "Breadcrumbs": "Breadcrumbs",
   "Browser notifications": "Notificacións do navegador",
-  "By @{group}": "Por @{group}",
-  "By @{username}": "Por @{username}",
+  "By {username}": "Por {username}",
   "By others": "Por outras",
   "By {author}": "Por {author}",
   "By {group}": "Por {group}",
diff --git a/js/src/i18n/hu.json b/js/src/i18n/hu.json
index b1ea212c2..cf6738e3a 100644
--- a/js/src/i18n/hu.json
+++ b/js/src/i18n/hu.json
@@ -97,8 +97,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Mielőtt bejelentkezne, rá kell kattintania a benne lévő hivatkozásra a fiókja ellenőrzéséhez.",
   "Begins on": "Ekkor kezdődik",
   "Bold": "Félkövér",
-  "By @{group}": "@{group} által",
-  "By @{username}": "@{username} által",
+  "By {username}": "{username} által",
   "By others": "Másoktól származó",
   "By {author}": "{author} által",
   "By {group}": "{group} által",
diff --git a/js/src/i18n/id.json b/js/src/i18n/id.json
index 42bdf9d4b..bdbe88a03 100644
--- a/js/src/i18n/id.json
+++ b/js/src/i18n/id.json
@@ -77,8 +77,7 @@
   "Big Blue Button": "Big Blue Button",
   "Bold": "Tebal",
   "Browser notifications": "Notifikasi browser",
-  "By @{group}": "Oleh @{group}",
-  "By @{username}": "Oleh @{username}",
+  "By {username}": "Oleh {username}",
   "By others": "Dari orang lain",
   "By {author}": "Oleh {author}",
   "By {group}": "Oleh {group}",
diff --git a/js/src/i18n/it.json b/js/src/i18n/it.json
index 6d13f2275..b50af865e 100644
--- a/js/src/i18n/it.json
+++ b/js/src/i18n/it.json
@@ -97,8 +97,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Prima di poter accedere, è necessario cliccare sul collegamento al suo interno per convalidare il tuo account.",
   "Begins on": "Comincia a",
   "Bold": "Grassetto",
-  "By @{group}": "Di @{group}",
-  "By @{username}": "Da @{username}",
+  "By {username}": "Da {username}",
   "By {author}": "Da {author}",
   "By {group}": "Da {group}",
   "Can be an email or a link, or just plain text.": "Può essere un'email o un collegamento, oppure testo semplice.",
diff --git a/js/src/i18n/nl.json b/js/src/i18n/nl.json
index f8a0372d2..12d4dc460 100644
--- a/js/src/i18n/nl.json
+++ b/js/src/i18n/nl.json
@@ -33,7 +33,7 @@
   "Are you sure you want to delete this event? This action cannot be reverted.": "Bent u zeker dat u dit evenement wil verwijderen? Dit kan niet ongedaan gemaakt worden.",
   "Avatar": "Profielfoto",
   "Before you can login, you need to click on the link inside it to validate your account.": "Voordat u zich kan aanmelden, moet u op de link erin klikken om uw account te valideren.",
-  "By @{username}": "Door @{username}",
+  "By {username}": "Door {username}",
   "Cancel": "Annuleren",
   "Cancel creation": "Aanmaken annuleren",
   "Cancel edition": "Bewerken annuleren",
diff --git a/js/src/i18n/nn.json b/js/src/i18n/nn.json
index 9644f81a1..ea9808cf0 100644
--- a/js/src/i18n/nn.json
+++ b/js/src/i18n/nn.json
@@ -135,11 +135,10 @@
   "Breadcrumbs": "Navigeringsmerke",
   "Browser notifications": "Nettlesarvarsel",
   "Bullet list": "Punktliste",
-  "By @{group}": "Av @{group}",
-  "By @{username}": "Av @{username}",
+  "By {username}": "Av {username}",
   "By others": "Av andre",
   "By {author}": "Av {author}",
-  "By {group}": "Av {gruppe}",
+  "By {group}": "Av {group}",
   "Can be an email or a link, or just plain text.": "Kan vera ei epostadresse eller ei lenke, eller rein tekst.",
   "Cancel": "Avbryt",
   "Cancel anonymous participation": "Avbryt anonym deltaking",
diff --git a/js/src/i18n/oc.json b/js/src/i18n/oc.json
index 4e9721b3b..af375b931 100644
--- a/js/src/i18n/oc.json
+++ b/js/src/i18n/oc.json
@@ -100,8 +100,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Abans que poscatz vos marcar, devètz clicar lo ligam dedins per validar lo compte.",
   "Begins on": "Comença lo",
   "Bold": "Gras",
-  "By @{group}": "Per @{group}",
-  "By @{username}": "Per @{username}",
+  "By {username}": "Per {username}",
   "By @{username} and @{group}": "Per @{username} e @{group}",
   "By {author}": "Per {author}",
   "By {group}": "Per {group}",
diff --git a/js/src/i18n/pl.json b/js/src/i18n/pl.json
index 7aa74282e..f5734a9ec 100644
--- a/js/src/i18n/pl.json
+++ b/js/src/i18n/pl.json
@@ -87,8 +87,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Zanim się zalogujesz, musisz odwiedzić odnośnik znajdujący się w niej, aby potwierdzić swoje konto.",
   "Begins on": "Zaczyna się",
   "Bold": "Pogrubione",
-  "By @{group}": "Od @{group}",
-  "By @{username}": "Od @{username}",
+  "By {username}": "Od {username}",
   "By {author}": "Autorstwa {author}",
   "By {group}": "Autorstwa {group}",
   "Can be an email or a link, or just plain text.": "Może być adresem e-mail, odnośnikiem lub zwykłym tekstem.",
diff --git a/js/src/i18n/pt.json b/js/src/i18n/pt.json
index 0940180be..6231cd837 100644
--- a/js/src/i18n/pt.json
+++ b/js/src/i18n/pt.json
@@ -33,7 +33,7 @@
   "Avatar": "Avatar",
   "Back to previous page": "Voltar à página anterior",
   "Before you can login, you need to click on the link inside it to validate your account.": "Antes de entrar, precisas de clicar no link dentro para validar a tua conta.",
-  "By @{username}": "Por @{username}",
+  "By {username}": "Por {username}",
   "Cancel": "Cancelar",
   "Cancel anonymous participation": "Cancelar participação anónima",
   "Cancel creation": "Aborto",
diff --git a/js/src/i18n/pt_BR.json b/js/src/i18n/pt_BR.json
index 6d1e12562..567c24eae 100644
--- a/js/src/i18n/pt_BR.json
+++ b/js/src/i18n/pt_BR.json
@@ -90,8 +90,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Antes de você entrar, você precisa clicar no link interno para validar a sua conta.",
   "Begins on": "Começa às",
   "Bold": "Negrito",
-  "By @{group}": "Por @{group}",
-  "By @{username}": "Por @{username}",
+  "By {username}": "Por {username}",
   "By {author}": "Por {author}",
   "By {group}": "Por {group}",
   "Can be an email or a link, or just plain text.": "Pode ser um email ou um link, ou apenas um texto simples.",
diff --git a/js/src/i18n/ru.json b/js/src/i18n/ru.json
index 36f0c7959..3f9b29a11 100644
--- a/js/src/i18n/ru.json
+++ b/js/src/i18n/ru.json
@@ -133,8 +133,7 @@
   "Breadcrumbs": "Хлебные крошки",
   "Browser notifications": "Уведомления в браузере",
   "Bullet list": "Маркированный список",
-  "By @{group}": "Из @{group}",
-  "By @{username}": "От @{username}",
+  "By {username}": "От {username}",
   "By others": "Другими",
   "By {author}": "Автор {author}",
   "By {group}": "Автор: {group}",
diff --git a/js/src/i18n/sl.json b/js/src/i18n/sl.json
index 3d7a83696..827d97a5a 100644
--- a/js/src/i18n/sl.json
+++ b/js/src/i18n/sl.json
@@ -94,8 +94,7 @@
   "Before you can login, you need to click on the link inside it to validate your account.": "Preden se lahko prijavite, morate klikniti na povezavo znotraj njega, da potrdite svoj račun.",
   "Begins on": "Začne se v",
   "Bold": "Krepko",
-  "By @{group}": "Avtor @{group}",
-  "By @{username}": "Od @{username}",
+  "By {username}": "Od {username}",
   "By others": "Od drugih",
   "By {author}": "Od {author}",
   "By {group}": "Od {group}",
diff --git a/js/src/i18n/sv.json b/js/src/i18n/sv.json
index 4d37b5723..75ec4a770 100644
--- a/js/src/i18n/sv.json
+++ b/js/src/i18n/sv.json
@@ -39,8 +39,8 @@
   "Avatar": "Avatar",
   "Back to previous page": "Tillbaka till föregående sida",
   "Before you can login, you need to click on the link inside it to validate your account.": "Innan du loggar in måste du klicka på länken inuti det för att validera ditt konto.",
-  "By @{group}": "Av @{group}",
-  "By @{username}": "Av @{username}",
+  "By {group}": "Av {group}",
+  "By {username}": "Av {username}",
   "By {username} and {group}": "Av {username} och {group}",
   "Cancel": "Avbryt",
   "Cancel anonymous participation": "Avbryt anonymt deltagande",
diff --git a/js/src/utils/i18n.ts b/js/src/utils/i18n.ts
index 01c003f09..7ec7e7eba 100644
--- a/js/src/utils/i18n.ts
+++ b/js/src/utils/i18n.ts
@@ -67,6 +67,12 @@ function setLanguageInDOM(lang: string): void {
   if (documentLang !== fixedLang) {
     html.setAttribute("lang", fixedLang);
   }
+
+  const direction = ["ar", "ae", "he", "fa", "ku", "ur"].includes(fixedLang)
+    ? "rtl"
+    : "ltr";
+  console.debug("setDirection with", [fixedLang, direction]);
+  html.setAttribute("dir", direction);
 }
 
 function fileForLanguage(matches: Record<string, string>, lang: string) {
diff --git a/js/src/views/About/AboutInstance.vue b/js/src/views/About/AboutInstance.vue
index 433a9a716..a301fde04 100644
--- a/js/src/views/About/AboutInstance.vue
+++ b/js/src/views/About/AboutInstance.vue
@@ -3,8 +3,8 @@
     <section class="hero is-primary">
       <div class="hero-body">
         <div class="container">
-          <h1 class="title">{{ config.name }}</h1>
-          <p>{{ config.description }}</p>
+          <h1 class="title" dir="auto">{{ config.name }}</h1>
+          <p dir="auto">{{ config.description }}</p>
         </div>
       </div>
     </section>
diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue
index 611e1f6da..802351c2d 100644
--- a/js/src/views/Event/Edit.vue
+++ b/js/src/views/Event/Edit.vue
@@ -28,6 +28,7 @@
             required
             v-model="event.title"
             id="title"
+            dir="auto"
           />
         </b-field>
 
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index 8adcd56ff..8154cbdd8 100755
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -7,7 +7,7 @@
         <div class="date-calendar-icon-wrapper">
           <date-calendar-icon :date="event.beginsOn" />
         </div>
-        <section class="intro">
+        <section class="intro" dir="auto">
           <div class="columns">
             <div class="column">
               <h1 class="title" style="margin: 0" dir="auto">
@@ -19,57 +19,24 @@
                     :actor="event.organizerActor"
                     :inline="true"
                   >
-                    <span>
-                      {{
-                        $t("By @{username}", {
-                          username: usernameWithDomain(event.organizerActor),
-                        })
-                      }}
-                    </span>
+                    <i18n path="By {username}" dir="auto">
+                      <span dir="ltr" slot="username"
+                        >@{{ usernameWithDomain(event.organizerActor) }}</span
+                      >
+                    </i18n>
                   </popover-actor-card>
                 </div>
-                <span
-                  v-else-if="
-                    event.attributedTo &&
-                    event.options.hideOrganizerWhenGroupEvent
-                  "
-                >
+                <span v-else-if="event.attributedTo">
                   <popover-actor-card
                     :actor="event.attributedTo"
                     :inline="true"
                   >
-                    {{
-                      $t("By @{group}", {
-                        group: usernameWithDomain(event.attributedTo),
-                      })
-                    }}
-                  </popover-actor-card>
-                </span>
-                <span v-else-if="event.organizerActor && event.attributedTo">
-                  <i18n path="By {group}">
-                    <popover-actor-card
-                      :actor="event.attributedTo"
-                      slot="group"
-                      :inline="true"
-                    >
-                      <router-link
-                        :to="{
-                          name: RouteName.GROUP,
-                          params: {
-                            preferredUsername: usernameWithDomain(
-                              event.attributedTo
-                            ),
-                          },
-                        }"
+                    <i18n path="By {group}" dir="auto">
+                      <span dir="ltr" slot="group"
+                        >@{{ usernameWithDomain(event.attributedTo) }}</span
                       >
-                        {{
-                          $t("@{group}", {
-                            group: usernameWithDomain(event.attributedTo),
-                          })
-                        }}
-                      </router-link>
-                    </popover-actor-card>
-                  </i18n>
+                    </i18n>
+                  </popover-actor-card>
                 </span>
               </div>
               <p
diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue
index 36d45d378..7f12f6207 100644
--- a/js/src/views/Group/Group.vue
+++ b/js/src/views/Group/Group.vue
@@ -60,7 +60,10 @@
           <div class="title-container">
             <h1 v-if="group.name">{{ group.name }}</h1>
             <b-skeleton v-else :animated="true" />
-            <small class="has-text-grey-dark" v-if="group.preferredUsername"
+            <small
+              dir="ltr"
+              class="has-text-grey-dark"
+              v-if="group.preferredUsername"
               >@{{ usernameWithDomain(group) }}</small
             >
             <b-skeleton v-else :animated="true" />
@@ -503,7 +506,7 @@
               }}</span>
               <div class="address" v-if="physicalAddress">
                 <div>
-                  <address>
+                  <address dir="auto">
                     <p
                       class="addressDescription"
                       :title="physicalAddress.poiInfos.name"
@@ -533,6 +536,7 @@
         <section>
           <subtitle>{{ $t("About") }}</subtitle>
           <div
+            dir="auto"
             v-html="group.summary"
             v-if="group.summary && group.summary !== '<p></p>'"
           />
diff --git a/js/src/views/Posts/Edit.vue b/js/src/views/Posts/Edit.vue
index 7d1a3182b..8dcb2aadd 100644
--- a/js/src/views/Posts/Edit.vue
+++ b/js/src/views/Posts/Edit.vue
@@ -64,6 +64,7 @@
             required
             v-model="editablePost.title"
             id="post-title"
+            dir="auto"
           />
         </b-field>
 
diff --git a/js/src/views/Search.vue b/js/src/views/Search.vue
index 93e7d607c..885368b21 100644
--- a/js/src/views/Search.vue
+++ b/js/src/views/Search.vue
@@ -20,6 +20,7 @@
               id="search"
               :value="search"
               @input="debouncedUpdateSearchQuery"
+              dir="auto"
               :placeholder="
                 $t('For instance: London, Taekwondo, Architecture…')
               "
diff --git a/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap b/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap
index 6bbf41328..258a06f7b 100644
--- a/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap
+++ b/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap
@@ -1,11 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`PostListItem renders post list item with basic informations 1`] = `
-<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper">
+<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
   <!---->
   <div class="title-info-wrapper has-text-grey-dark">
     <h3 class="post-minimalist-title">My Blog Post</h3>
-    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span class="has-text-grey-dark">Dec 2, 2020</span></p>
+    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
     <!---->
     <!---->
   </div>
@@ -13,11 +13,11 @@ exports[`PostListItem renders post list item with basic informations 1`] = `
 `;
 
 exports[`PostListItem renders post list item with publisher name 1`] = `
-<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper">
+<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
   <!---->
   <div class="title-info-wrapper has-text-grey-dark">
     <h3 class="post-minimalist-title">My Blog Post</h3>
-    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span class="has-text-grey-dark">Dec 2, 2020</span></p>
+    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
     <!---->
     <p class="post-publisher has-text-grey-dark"><span class="icon"><i class="mdi mdi-account-edit mdi-24px"></i></span> <span>Published by <b class="has-text-weight-medium">An author</b></span></p>
   </div>
@@ -25,11 +25,11 @@ exports[`PostListItem renders post list item with publisher name 1`] = `
 `;
 
 exports[`PostListItem renders post list item with tags 1`] = `
-<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper">
+<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
   <!---->
   <div class="title-info-wrapper has-text-grey-dark">
     <h3 class="post-minimalist-title">My Blog Post</h3>
-    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span class="has-text-grey-dark">Dec 2, 2020</span></p>
+    <p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
     <div class="tags" style="display: inline;"><span class="icon"><i class="mdi mdi-tag mdi-24px"></i></span> <span class="tag"><!----><span class="">A tag</span>
       <!----></span>
     </div>
diff --git a/lib/web/views/utils.ex b/lib/web/views/utils.ex
index 93336f44f..ab1dad8b1 100644
--- a/lib/web/views/utils.ex
+++ b/lib/web/views/utils.ex
@@ -49,11 +49,20 @@ defmodule Mobilizon.Web.Views.Utils do
   defp do_replacements(index_content, tags, locale) do
     index_content
     |> replace_meta(tags)
-    |> String.replace("<html lang=\"en\">", "<html lang=\"#{locale}\">")
+    |> String.replace(
+      "<html lang=\"en\" dir=\"auto\">",
+      "<html lang=\"#{locale}\" dir=\"#{get_language_direction(locale)}\">"
+    )
   end
 
   @spec get_locale(Plug.Conn.t()) :: String.t()
   def get_locale(%Plug.Conn{assigns: assigns}) do
     Map.get(assigns, :locale)
   end
+
+  @ltr_languages ["ar", "ae", "he", "fa", "ku", "ur"]
+
+  @spec get_language_direction(String.t()) :: String.t()
+  defp get_language_direction(locale) when locale in @ltr_languages, do: "rtl"
+  defp get_language_direction(_locale), do: "ltr"
 end