From 857f9cd8db05433805f14e1b77b1daad601ddbd5 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 31 Aug 2022 22:33:19 +0200
Subject: [PATCH] Make language settings accessible through GraphQL and set
 locale directly in Vue

---
 aleksis/core/assets/app.js                    | 18 ++--
 .../core/assets/components/LanguageForm.vue   | 99 +++++++++----------
 .../components/availableLanguages.graphql     |  7 ++
 aleksis/core/schema.py                        | 26 +++++
 aleksis/core/templates/core/vue_base.html     |  5 +-
 aleksis/core/util/frontend_helpers.py         | 17 ++++
 6 files changed, 107 insertions(+), 65 deletions(-)
 create mode 100644 aleksis/core/assets/components/availableLanguages.graphql

diff --git a/aleksis/core/assets/app.js b/aleksis/core/assets/app.js
index 31555d16d..43ec6d3d5 100644
--- a/aleksis/core/assets/app.js
+++ b/aleksis/core/assets/app.js
@@ -5,6 +5,7 @@ import "vuetify/dist/vuetify.min.css"
 
 import ApolloClient from 'apollo-boost'
 import VueApollo from 'vue-apollo'
+import gql from 'graphql-tag'
 
 import "./css/global.scss"
 import VueI18n from 'vue-i18n'
@@ -14,9 +15,8 @@ import messages from "./messages.json"
 Vue.use(VueI18n)
 
 const i18n = new VueI18n({
-    locale: JSON.parse(document.getElementById("current-language").textContent),
+    locale: "en",
     fallbackLocale: "en",
-    availableLocales: JSON.parse(document.getElementById("language-info-list").textContent),
     messages
 });
 
@@ -64,10 +64,6 @@ const vuetify = new Vuetify({
             },
         },
     },
-    lang: {
-        locales: JSON.parse(document.getElementById("language-info-list").textContent),
-        current: JSON.parse(document.getElementById("current-language").textContent),
-    }
 })
 
 const apolloClient = new ApolloClient({
@@ -107,8 +103,16 @@ const app = new Vue({
         django: window.django,
         // FIXME: maybe just use window.django in every component or find a suitable way to access this property everywhere
         showCacheAlert: false,
-        languageCode: JSON.parse(document.getElementById("current-language").textContent),
     }),
+    apollo: {
+        language: gql`{ language }`,
+    },
+    watch: {
+        language: function (newLanguage) {
+            this.$i18n.locale = newLanguage;
+            this.$vuetify.lang.current = newLanguage;
+        }
+    },
     components: {
         "cache-notification": CacheNotification,
         "language-form": LanguageForm,
diff --git a/aleksis/core/assets/components/LanguageForm.vue b/aleksis/core/assets/components/LanguageForm.vue
index 193329327..b1bb11127 100644
--- a/aleksis/core/assets/components/LanguageForm.vue
+++ b/aleksis/core/assets/components/LanguageForm.vue
@@ -1,64 +1,55 @@
 <template>
-  <form method="post" ref="form" :action="action" id="language-form">
-    <v-text-field
-      v-show="false"
-      name="csrfmiddlewaretoken"
-      :value="csrf_value"
-      type="hidden"
-    ></v-text-field>
-    <v-text-field
-      v-show="false"
-      name="next"
-      :value="next_url"
-      type="hidden"
-    ></v-text-field>
-    <v-text-field
-      v-show="false"
-      v-model="language"
-      name="language"
-      type="hidden"
-    ></v-text-field>
-    <v-menu offset-y>
-      <template v-slot:activator="{ on, attrs }">
-        <v-btn
+  <v-menu offset-y>
+    <template v-slot:activator="{ on, attrs }">
+      <v-btn
           depressed
           v-bind="attrs"
           v-on="on"
           color="primary"
-        >
-          <v-icon icon color="white">mdi-translate</v-icon>
-          {{ language }}
-        </v-btn>
-      </template>
-      <v-list id="language-dropdown" class="dropdown-content">
-        <v-list-item-group
-          v-model="language"
-          color="primary"
-        >
-          <v-list-item v-for="language_option in items" :key="language_option[0]" :value="language_option[0]" @click="submit(language_option[0])">
-            <v-list-item-title>{{ language_option[1] }}</v-list-item-title>
-          </v-list-item>
-        </v-list-item-group>
-      </v-list>
-    </v-menu>
-  </form>
+      >
+        <v-icon icon color="white">mdi-translate</v-icon>
+        {{ $i18n.locale }}
+      </v-btn>
+    </template>
+    <v-list id="language-dropdown" class="dropdown-content" min-width="150">
+      <ApolloQuery :query="require('./availableLanguages.graphql')">
+        <template v-slot="{ result: { error, data }, isLoading }">
+          <v-skeleton-loader
+              v-if="isLoading"
+              class="mx-auto"
+              type="list-item, list-item, list-item"
+          ></v-skeleton-loader>
+          <v-list-item-group
+              v-if="!isLoading"
+              v-model="$i18n.locale"
+              color="primary"
+          >
+            <v-list-item v-for="languageOption in data.availableLanguages" :key="languageOption.code"
+                         :value="languageOption.code"
+                         @click="setLanguage(languageOption)">
+              <v-list-item-title>{{ languageOption.nameTranslated }}</v-list-item-title>
+            </v-list-item>
+          </v-list-item-group>
+        </template>
+      </ApolloQuery>
+    </v-list>
+  </v-menu>
 </template>
 
 <script>
-  export default {
-    data: () => ({
-        items: JSON.parse(document.getElementById("language-info-list").textContent),
-        language: JSON.parse(document.getElementById("current-language").textContent),
-    }),
-    methods: {
-        submit: function (new_language) {
-            this.language = new_language;
-            this.$nextTick(() => {
-                this.$refs.form.submit();
-            });
-        },
+export default {
+  data: function () {
+    return {
+      language: this.$i18n.locale
+    }
+  },
+  methods: {
+    setLanguage: function (languageOption) {
+      document.cookie = languageOption.cookie;
+      this.$i18n.locale = languageOption.code;
+      this.$vuetify.lang.current = languageOption.code;
     },
-    props: ["action", "csrf_value", "next_url"],
-    name: "language-form",
-  }
+  },
+  name: "language-form",
+}
 </script>
diff --git a/aleksis/core/assets/components/availableLanguages.graphql b/aleksis/core/assets/components/availableLanguages.graphql
new file mode 100644
index 000000000..b9ae5248f
--- /dev/null
+++ b/aleksis/core/assets/components/availableLanguages.graphql
@@ -0,0 +1,7 @@
+{
+  availableLanguages {
+    code
+    nameTranslated
+    cookie
+  }
+}
diff --git a/aleksis/core/schema.py b/aleksis/core/schema.py
index 7e637aa20..a27cfd3c7 100644
--- a/aleksis/core/schema.py
+++ b/aleksis/core/schema.py
@@ -1,10 +1,15 @@
+from django.conf import settings
+from django.utils import translation
+
 import graphene
+from graphene import ObjectType
 from graphene_django import DjangoObjectType
 from graphene_django.forms.mutation import DjangoModelFormMutation
 
 from .forms import PersonForm
 from .models import Group, Notification, Person
 from .util.core_helpers import get_app_module, get_app_packages, has_person
+from .util.frontend_helpers import get_language_cookie
 
 
 class NotificationType(DjangoObjectType):
@@ -27,6 +32,15 @@ class GroupType(DjangoObjectType):
         model = Group
 
 
+class LanguageType(ObjectType):
+    code = graphene.String(required=True)
+    name = graphene.String(required=True)
+    name_local = graphene.String(required=True)
+    name_translated = graphene.String(required=True)
+    bidi = graphene.Boolean(required=True)
+    cookie = graphene.String(required=True)
+
+
 class PersonMutation(DjangoModelFormMutation):
     person = graphene.Field(PersonType)
 
@@ -59,6 +73,9 @@ class Query(graphene.ObjectType):
     person_by_id = graphene.Field(PersonType, id=graphene.ID())
     who_am_i = graphene.Field(PersonType)
 
+    language = graphene.String()
+    available_languages = graphene.List(LanguageType)
+
     def resolve_notifications(root, info, **kwargs):
         # FIXME do permission stuff
         return Notification.objects.all()
@@ -76,6 +93,15 @@ class Query(graphene.ObjectType):
         else:
             return None
 
+    def resolve_language(root, info, **kwargs):
+        return info.context.LANGUAGE_CODE
+
+    def resolve_available_languages(root, info, **kwargs):
+        return [
+            translation.get_language_info(code) | {"cookie": get_language_cookie(code)}
+            for code, name in settings.LANGUAGES
+        ]
+
 
 class Mutation(graphene.ObjectType):
     update_person = PersonMutation.Field()
diff --git a/aleksis/core/templates/core/vue_base.html b/aleksis/core/templates/core/vue_base.html
index 037a76a30..3d8f6a7bf 100644
--- a/aleksis/core/templates/core/vue_base.html
+++ b/aleksis/core/templates/core/vue_base.html
@@ -98,8 +98,7 @@
       </v-toolbar-title>
 
       <v-spacer></v-spacer>
-      <language-form action="{% url "set_language" %}"
-                     csrf_value={{ csrf_token }} next_url={{ request.get_full_path }}></language-form>
+      <language-form></language-form>
       {% if user.is_authenticated %}
         <v-menu offset-y>
           <template v-slot:activator="{ on, attrs }">
@@ -231,8 +230,6 @@
 {{ request.user.person.preferences.theme__design|json_script:"design-mode" }}
 {{ request.site.preferences.theme__primary|json_script:"primary-color" }}
 {{ request.site.preferences.theme__secondary|json_script:"secondary-color" }}
-{{ LANGUAGE_CODE|json_script:"current-language" }}
-{{ LANGUAGES|json_script:"language-info-list" }}
 <script type="text/javascript" src="{% static 'js/search.js' %}"></script>
 {% render_bundle 'core' %}
 {% block extra_body %}{% endblock %}
diff --git a/aleksis/core/util/frontend_helpers.py b/aleksis/core/util/frontend_helpers.py
index 034b1a0be..664707500 100644
--- a/aleksis/core/util/frontend_helpers.py
+++ b/aleksis/core/util/frontend_helpers.py
@@ -1,5 +1,7 @@
 import os
 
+from django.conf import settings
+
 from .core_helpers import get_app_module, get_app_packages
 
 
@@ -13,3 +15,18 @@ def get_apps_with_assets():
             package = ".".join(app.split(".")[:-2])
             assets[package] = path
     return assets
+
+
+def get_language_cookie(code: str) -> str:
+    """Build a cookie string to set a new language."""
+    cookie_parts = [f"{settings.LANGUAGE_COOKIE_NAME}={code}"]
+    args = dict(
+        max_age=settings.LANGUAGE_COOKIE_AGE,
+        path=settings.LANGUAGE_COOKIE_PATH,
+        domain=settings.LANGUAGE_COOKIE_DOMAIN,
+        secure=settings.LANGUAGE_COOKIE_SECURE,
+        httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
+        samesite=settings.LANGUAGE_COOKIE_SAMESITE,
+    )
+    cookie_parts += [f"{k.replace('_', '-')}={v}" for k, v in args.items() if v]
+    return "; ".join(cookie_parts)
-- 
GitLab