Repository: openfun/marsha

Issue:
Use LTI launch_presentation_locale parameter
## Feature Request

**Is your feature request related to a problem or unsupported use case? Please describe.**

For now we have no idea about which locale to use in marsha. We use by default english locale and we have nothing to determine which local to use.

**Describe the solution you'd like**

in the LTI request there is a parameter `launch_presentation_locale` containing the locale we should use: 
```
launch_presentation_locale=en-US
Language, country and variant as represented using the IETF Best Practices for Tags for Identifying Languages (BCP-47) available at http://www.rfc-editor.org/rfc/bcp/bcp47.txt
```

**Discovery, Documentation, Adoption, Migration Strategy**

documentation about this parameter is available in the LTI spec, in the ` Basic Launch Data` chapter (there is no anchor to link it) => http://www.imsglobal.org/specs/ltiv1p1p1/implementation-guide#toc-2




Discussion:

Resolving patch:
diff --git a/src/backend/marsha/core/lti.py b/src/backend/marsha/core/lti.py
--- a/src/backend/marsha/core/lti.py
+++ b/src/backend/marsha/core/lti.py
@@ -261,6 +261,18 @@ def is_edx_format(self):
         """
         return re.search(r"^course-v[0-9]:.*$", self.context_id)
 
+    @property
+    def launch_presentation_locale(self):
+        """LTI launch_presentation_locale.
+
+        Returns
+        -------
+        string
+            The locale present in launch_presentation_locale or fallback to en
+
+        """
+        return self.request.POST.get("launch_presentation_locale", "en")
+
     def get_or_create_video(self):
         """Get or create the video targeted by the LTI launch request.
 
diff --git a/src/backend/marsha/core/views.py b/src/backend/marsha/core/views.py
--- a/src/backend/marsha/core/views.py
+++ b/src/backend/marsha/core/views.py
@@ -77,6 +77,7 @@ def get_context_data(self):
                     "context_id": lti.context_id,
                     "roles": lti.roles,
                     "course": lti.get_course_info(),
+                    "locale": lti.launch_presentation_locale,
                 }
             )
             try:


Test patch:
diff --git a/src/backend/marsha/core/tests/test_lti.py b/src/backend/marsha/core/tests/test_lti.py
--- a/src/backend/marsha/core/tests/test_lti.py
+++ b/src/backend/marsha/core/tests/test_lti.py
@@ -372,6 +372,53 @@ def test_lti_get_course_info(self):
             {"school_name": "ufr", "course_name": "mathematics", "course_run": None},
         )
 
+    def test_lti_launch_presentation_locale_fallback(self):
+        """Fallback on "en" locale if property is missing."""
+        ConsumerSiteLTIPassportFactory(
+            oauth_consumer_key="ABC123", consumer_site__domain="testserver"
+        )
+        data = {
+            "resource_link_id": "df7",
+            "context_id": "13245",
+            "roles": "Student",
+            "oauth_consumer_key": "ABC123",
+            "tool_consumer_instance_name": "ufr",
+            "context_title": "mathematics",
+        }
+        resource_id = uuid.uuid4()
+        request = self.factory.post(
+            "/lti/videos/{!s}".format(resource_id),
+            data,
+            HTTP_X_FORWARDED_PROTO="https",
+            HTTP_REFERER="http://testserver/lti-video/",
+        )
+        lti = LTI(request, resource_id)
+        self.assertEqual(lti.launch_presentation_locale, "en")
+
+    def test_lti_launch_presentation_locale(self):
+        """Return value from launch_presentation_locale property when present."""
+        ConsumerSiteLTIPassportFactory(
+            oauth_consumer_key="ABC123", consumer_site__domain="testserver"
+        )
+        data = {
+            "resource_link_id": "df7",
+            "context_id": "13245",
+            "roles": "Student",
+            "oauth_consumer_key": "ABC123",
+            "tool_consumer_instance_name": "ufr",
+            "context_title": "mathematics",
+            "launch_presentation_locale": "fr",
+        }
+        resource_id = uuid.uuid4()
+        request = self.factory.post(
+            "/lti/videos/{!s}".format(resource_id),
+            data,
+            HTTP_X_FORWARDED_PROTO="https",
+            HTTP_REFERER="http://testserver/lti-video/",
+        )
+        lti = LTI(request, resource_id)
+        self.assertEqual(lti.launch_presentation_locale, "fr")
+
 
 class PortabilityVideoLTITestCase(TestCase):
     """Test the portability of videos beween playlists and consumer sites.
diff --git a/src/backend/marsha/core/tests/test_views_lti_video.py b/src/backend/marsha/core/tests/test_views_lti_video.py
--- a/src/backend/marsha/core/tests/test_views_lti_video.py
+++ b/src/backend/marsha/core/tests/test_views_lti_video.py
@@ -42,6 +42,7 @@ def test_views_video_lti_post_instructor(self):
             "roles": "instructor",
             "oauth_consumer_key": passport.oauth_consumer_key,
             "user_id": "56255f3807599c377bf0e5bf072359fd",
+            "launch_presentation_locale": "fr",
         }
         with mock.patch.object(
             LTI, "verify", return_value=passport.consumer_site
@@ -62,6 +63,9 @@ def test_views_video_lti_post_instructor(self):
         self.assertEqual(jwt_token.payload["user_id"], data["user_id"])
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(
+            jwt_token.payload["locale"], data["launch_presentation_locale"]
+        )
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": "00001"},
@@ -127,6 +131,7 @@ def test_views_video_lti_post_student(self):
         self.assertEqual(jwt_token.payload["user_id"], data["user_id"])
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(jwt_token.payload["locale"], "en")
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": "00001"},
@@ -187,6 +192,7 @@ def test_views_video_lti_without_user_id_parameter(self):
         self.assertEqual(jwt_token.payload["video_id"], str(video.id))
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(jwt_token.payload["locale"], "en")
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": None},
@@ -285,6 +291,7 @@ def test_views_video_lti_with_timed_text(self):
         self.assertEqual(jwt_token.payload["video_id"], str(video.id))
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(jwt_token.payload["locale"], "en")
 
 
 class DevelopmentViewsTestCase(TestCase):
@@ -315,6 +322,7 @@ def test_views_video_lti_post_bypass_lti_student(self):
             "tool_consumer_instance_name": "ufr",
             "tool_consumer_instance_guid": "example.com",
             "user_id": "56255f3807599c377bf0e5bf072359fd",
+            "launch_presentation_locale": "fr",
         }
         response = self.client.post("/lti/videos/{!s}".format(video.pk), data)
         self.assertEqual(response.status_code, 200)
@@ -332,6 +340,9 @@ def test_views_video_lti_post_bypass_lti_student(self):
         self.assertEqual(jwt_token.payload["user_id"], data["user_id"])
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(
+            jwt_token.payload["locale"], data["launch_presentation_locale"]
+        )
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": None},
@@ -389,6 +400,7 @@ def test_views_video_lti_post_bypass_lti_instructor(self):
         self.assertEqual(jwt_token.payload["user_id"], data["user_id"])
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(jwt_token.payload["locale"], "en")
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": None},
@@ -444,6 +456,7 @@ def test_views_video_lti_post_bypass_lti_instructor_no_video(self):
         self.assertEqual(jwt_token.payload["user_id"], data["user_id"])
         self.assertEqual(jwt_token.payload["context_id"], data["context_id"])
         self.assertEqual(jwt_token.payload["roles"], [data["roles"]])
+        self.assertEqual(jwt_token.payload["locale"], "en")
         self.assertDictEqual(
             jwt_token.payload["course"],
             {"school_name": "ufr", "course_name": "mathematics", "course_run": "00001"},