ol-openedx-auto-select-language 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # Build artifacts
2
+ /dist/
3
+ /.pids
4
+ # Python files
5
+ /.venv/
6
+ /.idea/
7
+ __pycache__/
8
+ *.egg-info/
9
+ *.DS_Store
10
+ # Test Runtime
11
+ ol_test_requirements.txt
12
+ **/.coverage
13
+ **/test_root
14
+ coverage.xml
@@ -0,0 +1,28 @@
1
+ Copyright (C) 2022 MIT Open Learning
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ * Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: ol-openedx-auto-select-language
3
+ Version: 0.1.0
4
+ Summary: An Open edX plugin to auto-select the language based on course language settings.
5
+ Author: MIT Office of Digital Learning
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE.txt
8
+ Keywords: Python,edx
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: django>=4.0
11
+ Requires-Dist: djangorestframework>=3.14.0
12
+ Requires-Dist: edx-opaque-keys
13
+ Description-Content-Type: text/x-rst
14
+
15
+ OL Open edX Auto Select Language
16
+ ================================
17
+
18
+ An Open edX plugin to auto select the Open edX platform language based on the course language.
19
+
20
+ Purpose
21
+ *******
22
+
23
+ Auto select the Open edX platform language based on the course language. When enabled, users will see the static site content in the course's configured language.
24
+
25
+ Setup
26
+ =====
27
+
28
+ For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.
29
+
30
+ Installation required in:
31
+
32
+ * Studio (CMS)
33
+ * LMS
34
+
35
+ Configuration
36
+ =============
37
+
38
+ - Add the following configuration values to the config file in Open edX. For any release after Juniper, that config file is ``/edx/etc/lms.yml`` and ``/edx/etc/cms.yml``. If you're using ``private.py``, add these values to ``lms/envs/private.py`` and ``cms/envs/private.py``. These should be added to the top level. **Ask a fellow developer for these values.**
39
+
40
+ .. code-block:: python
41
+
42
+ # Enable auto language selection
43
+ ENABLE_AUTO_LANGUAGE_SELECTION: true
44
+
45
+ - For Tutor installations, these values can also be managed through a `custom Tutor plugin <https://docs.tutor.edly.io/tutorials/plugin.html#plugin-development-tutorial>`_.
46
+
47
+ Auto Language Selection
48
+ =======================
49
+
50
+ The plugin includes an auto language selection feature that automatically sets the user's language preference based on the course language. When enabled, users will see the static site content in the course's configured language.
51
+
52
+ To enable auto language selection:
53
+
54
+ 1. Set ``ENABLE_AUTO_LANGUAGE_SELECTION`` to ``true`` in your settings.
55
+
56
+ 2. Set ``SHARED_COOKIE_DOMAIN`` to your domain (e.g., ``.local.openedx.io`` for local tutor setup) to allow cookies to be shared between LMS and CMS.
57
+
58
+ **How it works:**
59
+
60
+ - **LMS**: The ``CourseLanguageCookieMiddleware`` automatically detects course URLs and sets the language preference based on the course's configured language.
61
+ - **CMS**: The ``CourseLanguageCookieResetMiddleware`` ensures Studio always uses English for the authoring interface.
62
+ - **Admin areas**: Admin URLs (``/admin``, ``/sysadmin``, instructor dashboards) are forced to use English regardless of course language.
63
+
64
+ MFE Integration
65
+ ===============
66
+
67
+ To make auto language selection work with Micro-Frontends (MFEs), you need to use a custom Footer component that handles language detection and switching.
68
+
69
+ **Setup:**
70
+
71
+ 1. Use the Footer component from `src/bridge/settings/openedx/mfe/slot_config/Footer.jsx <https://github.com/mitodl/ol-infrastructure/blob/main/src/bridge/settings/openedx/mfe/slot_config/Footer.jsx>`_ in the `ol-infrastructure <https://github.com/mitodl/ol-infrastructure>`_ repository.
72
+
73
+ 2. Enable auto language selection in each MFE by adding the following to their ``.env.development`` file:
74
+
75
+ .. code-block:: bash
76
+
77
+ ENABLE_AUTO_LANGUAGE_SELECTION="true"
78
+
79
+ 3. This custom Footer component:
80
+ - Detects the current course context in MFEs
81
+ - Automatically switches the MFE language based on the course's configured language
82
+ - Ensures consistent language experience across the platform
83
+
84
+ 4. Configure your MFE slot overrides to use this custom Footer component instead of the default one.
85
+
86
+ **Note:** The custom Footer is required because MFEs run as separate applications and need their own mechanism to detect and respond to course language settings. The environment variable must be set in each MFE's configuration for the feature to work properly.
87
+
88
+ License
89
+ *******
90
+
91
+ The code in this repository is licensed under the AGPL 3.0 unless
92
+ otherwise noted.
93
+
94
+ Please see `LICENSE.txt <LICENSE.txt>`_ for details.
@@ -0,0 +1,80 @@
1
+ OL Open edX Auto Select Language
2
+ ================================
3
+
4
+ An Open edX plugin to auto select the Open edX platform language based on the course language.
5
+
6
+ Purpose
7
+ *******
8
+
9
+ Auto select the Open edX platform language based on the course language. When enabled, users will see the static site content in the course's configured language.
10
+
11
+ Setup
12
+ =====
13
+
14
+ For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.
15
+
16
+ Installation required in:
17
+
18
+ * Studio (CMS)
19
+ * LMS
20
+
21
+ Configuration
22
+ =============
23
+
24
+ - Add the following configuration values to the config file in Open edX. For any release after Juniper, that config file is ``/edx/etc/lms.yml`` and ``/edx/etc/cms.yml``. If you're using ``private.py``, add these values to ``lms/envs/private.py`` and ``cms/envs/private.py``. These should be added to the top level. **Ask a fellow developer for these values.**
25
+
26
+ .. code-block:: python
27
+
28
+ # Enable auto language selection
29
+ ENABLE_AUTO_LANGUAGE_SELECTION: true
30
+
31
+ - For Tutor installations, these values can also be managed through a `custom Tutor plugin <https://docs.tutor.edly.io/tutorials/plugin.html#plugin-development-tutorial>`_.
32
+
33
+ Auto Language Selection
34
+ =======================
35
+
36
+ The plugin includes an auto language selection feature that automatically sets the user's language preference based on the course language. When enabled, users will see the static site content in the course's configured language.
37
+
38
+ To enable auto language selection:
39
+
40
+ 1. Set ``ENABLE_AUTO_LANGUAGE_SELECTION`` to ``true`` in your settings.
41
+
42
+ 2. Set ``SHARED_COOKIE_DOMAIN`` to your domain (e.g., ``.local.openedx.io`` for local tutor setup) to allow cookies to be shared between LMS and CMS.
43
+
44
+ **How it works:**
45
+
46
+ - **LMS**: The ``CourseLanguageCookieMiddleware`` automatically detects course URLs and sets the language preference based on the course's configured language.
47
+ - **CMS**: The ``CourseLanguageCookieResetMiddleware`` ensures Studio always uses English for the authoring interface.
48
+ - **Admin areas**: Admin URLs (``/admin``, ``/sysadmin``, instructor dashboards) are forced to use English regardless of course language.
49
+
50
+ MFE Integration
51
+ ===============
52
+
53
+ To make auto language selection work with Micro-Frontends (MFEs), you need to use a custom Footer component that handles language detection and switching.
54
+
55
+ **Setup:**
56
+
57
+ 1. Use the Footer component from `src/bridge/settings/openedx/mfe/slot_config/Footer.jsx <https://github.com/mitodl/ol-infrastructure/blob/main/src/bridge/settings/openedx/mfe/slot_config/Footer.jsx>`_ in the `ol-infrastructure <https://github.com/mitodl/ol-infrastructure>`_ repository.
58
+
59
+ 2. Enable auto language selection in each MFE by adding the following to their ``.env.development`` file:
60
+
61
+ .. code-block:: bash
62
+
63
+ ENABLE_AUTO_LANGUAGE_SELECTION="true"
64
+
65
+ 3. This custom Footer component:
66
+ - Detects the current course context in MFEs
67
+ - Automatically switches the MFE language based on the course's configured language
68
+ - Ensures consistent language experience across the platform
69
+
70
+ 4. Configure your MFE slot overrides to use this custom Footer component instead of the default one.
71
+
72
+ **Note:** The custom Footer is required because MFEs run as separate applications and need their own mechanism to detect and respond to course language settings. The environment variable must be set in each MFE's configuration for the feature to work properly.
73
+
74
+ License
75
+ *******
76
+
77
+ The code in this repository is licensed under the AGPL 3.0 unless
78
+ otherwise noted.
79
+
80
+ Please see `LICENSE.txt <LICENSE.txt>`_ for details.
@@ -0,0 +1,3 @@
1
+ """
2
+ MIT's Open edX auto language selection plugin
3
+ """
@@ -0,0 +1,34 @@
1
+ """
2
+ ol_openedx_auto_select_language Django application initialization.
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+ from edx_django_utils.plugins import PluginSettings, PluginURLs
7
+ from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
8
+
9
+
10
+ class OLOpenEdxAutoSelectLanguageConfig(AppConfig):
11
+ """
12
+ Configuration for the ol_openedx_auto_select_language Django application.
13
+ """
14
+
15
+ name = "ol_openedx_auto_select_language"
16
+ verbose_name = "OL Auto Select Language"
17
+
18
+ plugin_app = {
19
+ PluginURLs.CONFIG: {
20
+ ProjectType.LMS: {
21
+ PluginURLs.NAMESPACE: "",
22
+ PluginURLs.REGEX: "",
23
+ PluginURLs.RELATIVE_PATH: "urls",
24
+ }
25
+ },
26
+ PluginSettings.CONFIG: {
27
+ ProjectType.CMS: {
28
+ SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.cms"},
29
+ },
30
+ ProjectType.LMS: {
31
+ SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.lms"},
32
+ },
33
+ },
34
+ }
@@ -0,0 +1,3 @@
1
+ """Constants for ol_openedx_auto_select_language."""
2
+
3
+ ENGLISH_LANGUAGE_CODE = "en"
@@ -0,0 +1,58 @@
1
+ """
2
+ Filters for Open edX auto select language.
3
+ """
4
+
5
+ from openedx_filters import PipelineStep
6
+ from xmodule.modulestore.django import modulestore
7
+
8
+ from ol_openedx_auto_select_language.constants import (
9
+ ENGLISH_LANGUAGE_CODE,
10
+ )
11
+ from ol_openedx_auto_select_language.utils import LanguageCode
12
+
13
+ VIDEO_BLOCK_TYPE = "video"
14
+
15
+
16
+ class AddDestLangForVideoBlock(PipelineStep):
17
+ """
18
+ Pipeline step to add destination language for video transcripts
19
+ """
20
+
21
+ def run_filter(self, context, student_view_context):
22
+ """
23
+ Add the destination language to the student view context if a video block
24
+ with transcripts in the course language is found among the child blocks.
25
+ """
26
+
27
+ def set_dest_lang_for_video_block(block_key):
28
+ """
29
+ Set the destination language for video blocks with
30
+ transcripts in the course language.
31
+ """
32
+ student_view_context["dest_lang"] = (
33
+ ENGLISH_LANGUAGE_CODE # default to English
34
+ )
35
+ video_block = modulestore().get_item(block_key)
36
+ transcripts_info = video_block.get_transcripts_info()
37
+ dest_lang = getattr(
38
+ context.get("course", None), "language", ENGLISH_LANGUAGE_CODE
39
+ )
40
+ dest_lang = LanguageCode(dest_lang).to_bcp47()
41
+ if (
42
+ transcripts_info
43
+ and transcripts_info.get("transcripts", {})
44
+ and dest_lang in transcripts_info["transcripts"]
45
+ ):
46
+ student_view_context["dest_lang"] = dest_lang
47
+
48
+ block = dict(context)["block"]
49
+ block_usage_key = block.usage_key
50
+ block_type = block_usage_key.block_type
51
+ if block_type == "vertical":
52
+ for child in getattr(block, "children", []):
53
+ if child.block_type == VIDEO_BLOCK_TYPE:
54
+ set_dest_lang_for_video_block(child)
55
+ elif block_type == VIDEO_BLOCK_TYPE:
56
+ set_dest_lang_for_video_block(block_usage_key)
57
+
58
+ return {"context": context, "student_view_context": student_view_context}
@@ -0,0 +1,145 @@
1
+ """
2
+ Middleware to set/reset language preference cookie and
3
+ user preference based on course language.
4
+ """
5
+
6
+ import re
7
+
8
+ from django.conf import settings
9
+ from django.http import HttpResponseRedirect
10
+ from django.utils.deprecation import MiddlewareMixin
11
+ from opaque_keys.edx.keys import CourseKey
12
+ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
13
+ from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
14
+ from openedx.core.djangoapps.lang_pref import helpers as lang_pref_helpers
15
+ from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
16
+
17
+ from ol_openedx_auto_select_language.constants import ENGLISH_LANGUAGE_CODE
18
+ from ol_openedx_auto_select_language.utils import LanguageCode
19
+
20
+
21
+ def should_process_request(request):
22
+ """
23
+ Return True if language auto-selection should run for this request.
24
+ """
25
+ return (
26
+ settings.ENABLE_AUTO_LANGUAGE_SELECTION
27
+ and hasattr(request, "user")
28
+ and request.user.is_authenticated
29
+ )
30
+
31
+
32
+ def set_language(request, response, language):
33
+ """
34
+ Set both cookie and user preference for language.
35
+ """
36
+ lang_pref_helpers.set_language_cookie(request, response, language)
37
+ set_user_preference(request.user, LANGUAGE_KEY, language)
38
+
39
+
40
+ def redirect_current_path(request):
41
+ """
42
+ Redirect to the same URL to ensure language change takes effect.
43
+ """
44
+ return HttpResponseRedirect(request.get_full_path())
45
+
46
+
47
+ class CourseLanguageCookieMiddleware(MiddlewareMixin):
48
+ """
49
+ LMS middleware that:
50
+ - Sets language based on course language
51
+ - Forces English for exempt paths and authoring MFEs
52
+ """
53
+
54
+ COURSE_URL_REGEX = re.compile(
55
+ rf"^/courses/(?P<course_key>{settings.COURSE_KEY_REGEX})(?:/|$)",
56
+ re.IGNORECASE,
57
+ )
58
+
59
+ def process_response(self, request, response):
60
+ """
61
+ Process the response to set/reset language cookie based on course language.
62
+ """
63
+ if not should_process_request(request):
64
+ return response
65
+
66
+ path = getattr(request, "path_info", request.path)
67
+
68
+ if self._should_force_english(request, path):
69
+ return self._force_english_if_needed(request, response)
70
+
71
+ course_language = self._get_course_language(path)
72
+ if not course_language:
73
+ return response
74
+
75
+ return self._apply_course_language(request, response, course_language)
76
+
77
+ def _should_force_english(self, request, path):
78
+ """
79
+ Determine if English should be forced based on request origin or exempt paths.
80
+ """
81
+ return request.META.get(
82
+ "HTTP_ORIGIN"
83
+ ) == settings.COURSE_AUTHORING_MICROFRONTEND_URL or any(
84
+ exempt_path in path
85
+ for exempt_path in settings.AUTO_LANGUAGE_SELECTION_EXEMPT_PATHS
86
+ )
87
+
88
+ def _force_english_if_needed(self, request, response):
89
+ """
90
+ Force language to English if not already set.
91
+ """
92
+ cookie_val = lang_pref_helpers.get_language_cookie(request)
93
+
94
+ if cookie_val != ENGLISH_LANGUAGE_CODE:
95
+ set_language(request, response, ENGLISH_LANGUAGE_CODE)
96
+ return redirect_current_path(request)
97
+
98
+ return response
99
+
100
+ def _get_course_language(self, path):
101
+ """
102
+ Extract course language from the course URL path.
103
+ """
104
+ match = self.COURSE_URL_REGEX.match(path)
105
+ if not match:
106
+ return None
107
+
108
+ try:
109
+ course_key = CourseKey.from_string(match.group("course_key"))
110
+ overview = CourseOverview.get_from_id(course_key)
111
+ except Exception: # noqa: BLE001
112
+ return None
113
+
114
+ return getattr(overview, "language", None)
115
+
116
+ def _apply_course_language(self, request, response, language):
117
+ """
118
+ Apply the course language if it differs from the current cookie value.
119
+ """
120
+ language = LanguageCode(language).to_bcp47()
121
+ cookie_val = lang_pref_helpers.get_language_cookie(request)
122
+ if cookie_val != language:
123
+ set_language(request, response, language)
124
+ return redirect_current_path(request)
125
+
126
+ return response
127
+
128
+
129
+ class CourseLanguageCookieResetMiddleware(MiddlewareMixin):
130
+ """
131
+ CMS middleware that always resets language to English.
132
+ """
133
+
134
+ def process_response(self, request, response):
135
+ """
136
+ Process the response to reset language cookie to English.
137
+ """
138
+ if not should_process_request(request):
139
+ return response
140
+
141
+ cookie_val = lang_pref_helpers.get_language_cookie(request)
142
+ if cookie_val and cookie_val != ENGLISH_LANGUAGE_CODE:
143
+ set_language(request, response, ENGLISH_LANGUAGE_CODE)
144
+
145
+ return response
@@ -0,0 +1,17 @@
1
+ # noqa: INP001
2
+
3
+ """Settings to provide to edX"""
4
+
5
+ from ol_openedx_auto_select_language.settings.common import apply_common_settings
6
+
7
+
8
+ def plugin_settings(settings):
9
+ """
10
+ Populate cms settings
11
+ """
12
+ apply_common_settings(settings)
13
+ settings.MIDDLEWARE.extend(
14
+ [
15
+ "ol_openedx_auto_select_language.middleware.CourseLanguageCookieResetMiddleware"
16
+ ]
17
+ )
@@ -0,0 +1,14 @@
1
+ # noqa: INP001
2
+
3
+ """Common settings for LMS and CMS to provide to edX"""
4
+
5
+
6
+ def apply_common_settings(settings):
7
+ """
8
+ Apply custom settings function for LMS and CMS settings.
9
+
10
+ Args:
11
+ settings: Django settings object to modify
12
+ """
13
+ settings.ENABLE_AUTO_LANGUAGE_SELECTION = False
14
+ settings.AUTO_LANGUAGE_SELECTION_EXEMPT_PATHS = ["admin", "sysadmin", "instructor"]
@@ -0,0 +1,38 @@
1
+ # noqa: INP001
2
+
3
+ """Settings to provide to edX"""
4
+
5
+ from ol_openedx_auto_select_language.settings.common import apply_common_settings
6
+
7
+
8
+ def plugin_settings(settings):
9
+ """
10
+ Populate lms settings
11
+ """
12
+ apply_common_settings(settings)
13
+ settings.MIDDLEWARE.extend(
14
+ ["ol_openedx_auto_select_language.middleware.CourseLanguageCookieMiddleware"]
15
+ )
16
+ VIDEO_TRANSCRIPT_LANGUAGE_FILTERS = {
17
+ "org.openedx.learning.xblock.render.started.v1": {
18
+ "pipeline": [
19
+ "ol_openedx_auto_select_language.filters.AddDestLangForVideoBlock"
20
+ ],
21
+ "fail_silently": False,
22
+ }
23
+ }
24
+ existing_filters = getattr(settings, "OPEN_EDX_FILTERS_CONFIG", {})
25
+
26
+ # Merge pipeline lists instead of overwriting
27
+ for filter_name, config in VIDEO_TRANSCRIPT_LANGUAGE_FILTERS.items():
28
+ if filter_name not in existing_filters:
29
+ existing_filters[filter_name] = config
30
+ else:
31
+ existing_filters[filter_name]["pipeline"].extend(config.get("pipeline", []))
32
+ # do not override fail_silently
33
+ if "fail_silently" in config:
34
+ existing_filters[filter_name].setdefault(
35
+ "fail_silently", config["fail_silently"]
36
+ )
37
+
38
+ settings.OPEN_EDX_FILTERS_CONFIG = existing_filters
@@ -0,0 +1,23 @@
1
+ """
2
+ URL configuration for ol_openedx_auto_select_language app.
3
+ """
4
+
5
+ from django.conf import settings
6
+ from django.urls import re_path
7
+
8
+ from ol_openedx_auto_select_language.views import CourseLanguageView
9
+
10
+ urlpatterns = [
11
+ re_path(
12
+ rf"auto-select-language/api/course-language/{settings.COURSE_KEY_PATTERN}$",
13
+ CourseLanguageView.as_view(),
14
+ name="ol_course_language",
15
+ ),
16
+ # TODO: Remove the legacy endpoint in a # noqa: FIX002, TD003, TD002
17
+ # future release after updating all clients to use the new endpoint.
18
+ re_path(
19
+ rf"course-translations/api/course-language/{settings.COURSE_KEY_PATTERN}$",
20
+ CourseLanguageView.as_view(),
21
+ name="ol_course_language_legacy",
22
+ ),
23
+ ]
@@ -0,0 +1,96 @@
1
+ """
2
+ Utils for ol_openedx_auto_select_language.
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ class LanguageCode:
9
+ """
10
+ Utility class for handling language code conversions between
11
+ Django/Open edX style and BCP47.
12
+ """
13
+
14
+ def __init__(self, lang_code):
15
+ self.lang_code = lang_code
16
+
17
+ def to_bcp47(self) -> str:
18
+ """
19
+ Convert Django / Open edX style language codes to BCP47.
20
+
21
+ Examples:
22
+ zh_HANS -> zh-Hans
23
+ zh_HANT -> zh-Hant
24
+ zh_HANS_CN -> zh-Hans-CN
25
+ en_US -> en-US
26
+ es_419 -> es-419
27
+ pt_br -> pt-BR
28
+ """
29
+ if not self.lang_code:
30
+ return self.lang_code
31
+
32
+ parts = self.lang_code.replace("_", "-").split("-")
33
+ result = []
34
+ for idx, part in enumerate(parts):
35
+ if idx == 0:
36
+ # Language
37
+ result.append(part.lower())
38
+
39
+ elif re.fullmatch(r"[A-Za-z]{4}", part):
40
+ # Script (Hans, Hant, Latn, Cyrl, etc.)
41
+ result.append(part.title())
42
+
43
+ elif re.fullmatch(r"[A-Za-z]{2}", part):
44
+ # Region i.e US, PK, CN
45
+ result.append(part.upper())
46
+
47
+ elif re.fullmatch(r"\d{3}", part):
48
+ # Numeric region (419)
49
+ result.append(part)
50
+
51
+ else:
52
+ # Variants/extensions
53
+ result.append(part.lower())
54
+
55
+ return "-".join(result)
56
+
57
+ def to_django(self) -> str:
58
+ """
59
+ Convert BCP47 language tags to Django / Open edX style.
60
+
61
+ Examples:
62
+ zh-Hans -> zh_HANS
63
+ zh-Hant -> zh_HANT
64
+ zh-Hans-CN -> zh_HANS_CN
65
+ en-US -> en_US
66
+ es-419 -> es_419
67
+ pt-BR -> pt_BR
68
+ """
69
+ if not self.lang_code:
70
+ return self.lang_code
71
+
72
+ parts = self.lang_code.replace("_", "-").split("-")
73
+ result = []
74
+
75
+ for idx, part in enumerate(parts):
76
+ if idx == 0:
77
+ # Language
78
+ result.append(part.lower())
79
+
80
+ elif re.fullmatch(r"[A-Za-z]{4}", part):
81
+ # Script
82
+ result.append(part.upper())
83
+
84
+ elif re.fullmatch(r"[A-Za-z]{2}", part):
85
+ # Region
86
+ result.append(part.upper())
87
+
88
+ elif re.fullmatch(r"\d{3}", part):
89
+ # Numeric region
90
+ result.append(part)
91
+
92
+ else:
93
+ # Variants/extensions
94
+ result.append(part.lower())
95
+
96
+ return "_".join(result)
@@ -0,0 +1,76 @@
1
+ """
2
+ API Views for ol_openedx_auto_select_language App
3
+ """
4
+
5
+ import logging
6
+
7
+ from opaque_keys import InvalidKeyError
8
+ from opaque_keys.edx.keys import CourseKey
9
+ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
10
+ from rest_framework import status
11
+ from rest_framework.permissions import IsAuthenticated
12
+ from rest_framework.response import Response
13
+ from rest_framework.views import APIView
14
+
15
+ from ol_openedx_auto_select_language.utils import LanguageCode
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class CourseLanguageView(APIView):
21
+ """
22
+ API View to retrieve the language of a specified course.
23
+
24
+ Sample Request:
25
+ GET /auto-select-language/api/course_language/{course_key}/
26
+
27
+ Sample Response:
28
+ 200 OK
29
+ {
30
+ "language": "en"
31
+ }
32
+
33
+ Error Responses:
34
+ 400 Bad Request
35
+ {
36
+ "error": "Invalid course_key."
37
+ }
38
+ 404 Not Found
39
+ {
40
+ "error": "Course not found."
41
+ }
42
+ 400 Bad Request
43
+ {
44
+ "error": "An unexpected error occurred."
45
+ }
46
+ """
47
+
48
+ permission_classes = [IsAuthenticated]
49
+
50
+ def get(self, request, course_key_string): # noqa: ARG002
51
+ """
52
+ Retrieve the language of the specified course.
53
+ """
54
+ try:
55
+ course_key = CourseKey.from_string(course_key_string)
56
+ course = CourseOverview.get_from_id(course_key)
57
+ except InvalidKeyError:
58
+ log.info("Invalid course key %s", course_key_string)
59
+ return Response(
60
+ {"error": "Invalid course_key."},
61
+ status=status.HTTP_400_BAD_REQUEST,
62
+ )
63
+ except CourseOverview.DoesNotExist:
64
+ log.info("Course not found for key %s", course_key_string)
65
+ return Response(
66
+ {"error": "Course not found."},
67
+ status=status.HTTP_404_NOT_FOUND,
68
+ )
69
+ except Exception:
70
+ log.exception("Unexpected error retrieving course %s", course_key_string)
71
+ return Response(
72
+ {"error": "An unexpected error occurred."},
73
+ status=status.HTTP_400_BAD_REQUEST,
74
+ )
75
+ language = LanguageCode(course.language).to_bcp47()
76
+ return Response({"language": language})
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "ol-openedx-auto-select-language"
3
+ version = "0.1.0"
4
+ description = "An Open edX plugin to auto-select the language based on course language settings."
5
+ authors = [
6
+ {name = "MIT Office of Digital Learning"}
7
+ ]
8
+ license = "BSD-3-Clause"
9
+ readme = "README.rst"
10
+ requires-python = ">=3.11"
11
+ keywords = ["Python", "edx"]
12
+ dependencies = [
13
+ "Django>=4.0",
14
+ "djangorestframework>=3.14.0",
15
+ "edx-opaque-keys",
16
+ ]
17
+
18
+ [project.entry-points."cms.djangoapp"]
19
+ ol_openedx_auto_select_language = "ol_openedx_auto_select_language.apps:OLOpenEdxAutoSelectLanguageConfig"
20
+
21
+ [project.entry-points."lms.djangoapp"]
22
+ ol_openedx_auto_select_language = "ol_openedx_auto_select_language.apps:OLOpenEdxAutoSelectLanguageConfig"
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["ol_openedx_auto_select_language"]
30
+ include = [
31
+ "ol_openedx_auto_select_language/**/*.py",
32
+ ]
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = [
36
+ "ol_openedx_auto_select_language/**/*",
37
+ "README.rst",
38
+ "pyproject.toml",
39
+ ]