ol-openedx-course-translations 0.1.0__py3-none-any.whl → 0.3.5__py3-none-any.whl

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.

Potentially problematic release.


This version of ol-openedx-course-translations might be problematic. Click here for more details.

Files changed (40) hide show
  1. ol_openedx_course_translations/admin.py +29 -0
  2. ol_openedx_course_translations/apps.py +13 -2
  3. ol_openedx_course_translations/filters.py +39 -0
  4. ol_openedx_course_translations/glossaries/machine_learning/ar.txt +175 -0
  5. ol_openedx_course_translations/glossaries/machine_learning/de.txt +175 -0
  6. ol_openedx_course_translations/glossaries/machine_learning/el.txt +988 -0
  7. ol_openedx_course_translations/glossaries/machine_learning/es.txt +175 -0
  8. ol_openedx_course_translations/glossaries/machine_learning/fr.txt +175 -0
  9. ol_openedx_course_translations/glossaries/machine_learning/ja.txt +175 -0
  10. ol_openedx_course_translations/glossaries/machine_learning/pt-br.txt +175 -0
  11. ol_openedx_course_translations/glossaries/machine_learning/ru.txt +213 -0
  12. ol_openedx_course_translations/management/commands/sync_and_translate_language.py +1866 -0
  13. ol_openedx_course_translations/management/commands/translate_course.py +472 -475
  14. ol_openedx_course_translations/middleware.py +143 -0
  15. ol_openedx_course_translations/migrations/0001_add_translation_logs.py +84 -0
  16. ol_openedx_course_translations/migrations/__init__.py +0 -0
  17. ol_openedx_course_translations/models.py +57 -0
  18. ol_openedx_course_translations/providers/__init__.py +1 -0
  19. ol_openedx_course_translations/providers/base.py +278 -0
  20. ol_openedx_course_translations/providers/deepl_provider.py +292 -0
  21. ol_openedx_course_translations/providers/llm_providers.py +581 -0
  22. ol_openedx_course_translations/settings/cms.py +17 -0
  23. ol_openedx_course_translations/settings/common.py +58 -30
  24. ol_openedx_course_translations/settings/lms.py +38 -0
  25. ol_openedx_course_translations/tasks.py +222 -0
  26. ol_openedx_course_translations/urls.py +16 -0
  27. ol_openedx_course_translations/utils/__init__.py +0 -0
  28. ol_openedx_course_translations/utils/command_utils.py +197 -0
  29. ol_openedx_course_translations/utils/constants.py +218 -0
  30. ol_openedx_course_translations/utils/course_translations.py +608 -0
  31. ol_openedx_course_translations/utils/translation_sync.py +808 -0
  32. ol_openedx_course_translations/views.py +73 -0
  33. ol_openedx_course_translations-0.3.5.dist-info/METADATA +409 -0
  34. ol_openedx_course_translations-0.3.5.dist-info/RECORD +40 -0
  35. ol_openedx_course_translations-0.3.5.dist-info/entry_points.txt +5 -0
  36. ol_openedx_course_translations-0.1.0.dist-info/METADATA +0 -63
  37. ol_openedx_course_translations-0.1.0.dist-info/RECORD +0 -11
  38. ol_openedx_course_translations-0.1.0.dist-info/entry_points.txt +0 -2
  39. {ol_openedx_course_translations-0.1.0.dist-info → ol_openedx_course_translations-0.3.5.dist-info}/WHEEL +0 -0
  40. {ol_openedx_course_translations-0.1.0.dist-info → ol_openedx_course_translations-0.3.5.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,581 @@
1
+ """LLM-based translation providers."""
2
+
3
+ import logging
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import srt
9
+ from django.conf import settings
10
+ from litellm import completion
11
+
12
+ from .base import TranslationProvider, load_glossary
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Human-readable language names for LLM prompts
17
+ LANGUAGE_DISPLAY_NAMES = {
18
+ "en": "English",
19
+ "de": "Deutsch",
20
+ "es": "Español",
21
+ "fr": "Français",
22
+ "pt-br": "Português - Brasil",
23
+ "ru": "Русский",
24
+ "hi": "हिंदी",
25
+ "el": "ελληνικά",
26
+ "ja": "日本語",
27
+ "ar": "العربية",
28
+ "zh": "中文",
29
+ "tr": "Türkçe",
30
+ "sq": "Shqip",
31
+ "kr": "한국어",
32
+ "id": "Bahasa Indonesia",
33
+ }
34
+
35
+ # LLM error detection keywords
36
+ LLM_ERROR_KEYWORDS = [
37
+ "token",
38
+ "quota",
39
+ "limit",
40
+ "too large",
41
+ "context_length_exceeded",
42
+ "503",
43
+ "timeout",
44
+ ]
45
+
46
+ # LLM explanation phrases to filter from responses
47
+ LLM_EXPLANATION_KEYWORDS = [
48
+ "here is",
49
+ "here's",
50
+ "translation:",
51
+ "translated text:",
52
+ "note:",
53
+ "explanation:",
54
+ "i have translated",
55
+ "i've translated",
56
+ ]
57
+ # Translation markers for structured responses
58
+ TRANSLATION_MARKER_START = ":::TRANSLATION_START:::"
59
+ TRANSLATION_MARKER_END = ":::TRANSLATION_END:::"
60
+
61
+
62
+ class LLMProvider(TranslationProvider):
63
+ """
64
+ Base class for LLM-based providers (OpenAI, Gemini) that use structured prompting.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ primary_api_key: str,
70
+ repair_api_key: str | None = None,
71
+ model_name: str | None = None,
72
+ timeout: int = settings.LITE_LLM_REQUEST_TIMEOUT,
73
+ ):
74
+ """
75
+ Initialize LLM provider with API keys and model name.
76
+
77
+ Args:
78
+ primary_api_key: API key for the LLM service
79
+ repair_api_key: API key for repair service (optional)
80
+ model_name: Name of the LLM model to use
81
+ """
82
+ super().__init__(primary_api_key, repair_api_key)
83
+ self.model_name = model_name
84
+ self.timeout = timeout
85
+
86
+ def _get_subtitle_system_prompt(
87
+ self,
88
+ target_language: str,
89
+ glossary_directory: str | None = None,
90
+ ) -> str:
91
+ """
92
+ Generate system prompt for subtitle translation.
93
+
94
+ Creates detailed prompts with rules for subtitle translation,
95
+ including glossary terms if provided.
96
+
97
+ Args:
98
+ target_language: Target language code
99
+ glossary_directory: Path to glossary directory (optional)
100
+
101
+ Returns:
102
+ System prompt string for subtitle translation
103
+ """
104
+ target_language_display_name = LANGUAGE_DISPLAY_NAMES.get(
105
+ target_language, target_language
106
+ )
107
+
108
+ system_prompt = (
109
+ f"You are a professional subtitle translator. "
110
+ f"Translate the following English subtitles to "
111
+ f"{target_language_display_name}.\n\n"
112
+ "INPUT FORMAT:\n"
113
+ ":::ID:::\n"
114
+ "Text to translate\n\n"
115
+ "OUTPUT FORMAT (exactly):\n"
116
+ ":::ID:::\n"
117
+ "Translated text\n\n"
118
+ "RULES:\n"
119
+ "1. Preserve ALL :::ID::: markers exactly as given.\n"
120
+ "2. Every input ID MUST appear in output with its translation.\n"
121
+ "3. One ID = one translation. "
122
+ "NEVER merge or split content across IDs.\n"
123
+ "4. Keep proper nouns, brand names, and acronyms unchanged.\n"
124
+ "5. Use natural phrasing appropriate for subtitles.\n"
125
+ )
126
+
127
+ if glossary_directory:
128
+ glossary_terms = load_glossary(target_language, glossary_directory)
129
+ if glossary_terms:
130
+ system_prompt += (
131
+ f"\nGLOSSARY TERMS (use these translations):\n{glossary_terms}\n"
132
+ )
133
+
134
+ return system_prompt
135
+
136
+ def _get_text_system_prompt(
137
+ self,
138
+ target_language: str,
139
+ glossary_directory: str | None = None,
140
+ ) -> str:
141
+ """
142
+ Generate system prompt for text/HTML/XML translation.
143
+
144
+ Creates detailed prompts with rules for text translation,
145
+ including glossary terms if provided.
146
+
147
+ Args:
148
+ target_language: Target language code
149
+ glossary_directory: Path to glossary directory (optional)
150
+
151
+ Returns:
152
+ System prompt string for text translation
153
+ """
154
+ target_language_display_name = LANGUAGE_DISPLAY_NAMES.get(
155
+ target_language, target_language
156
+ )
157
+
158
+ system_prompt = (
159
+ f"You are a professional translator. "
160
+ f"Translate the following English text to "
161
+ f"{target_language_display_name}.\n\n"
162
+ f"OUTPUT FORMAT (exactly):\n"
163
+ f"{TRANSLATION_MARKER_START}\n"
164
+ "Your translated text here\n"
165
+ f"{TRANSLATION_MARKER_END}\n\n"
166
+ "CRITICAL RULES FOR XML/HTML TAGS:\n"
167
+ "1. NEVER translate or modify XML/HTML tags, tag names, or attributes except display_name.\n" # noqa: E501
168
+ "2. XML/HTML tags include anything within angle brackets: < >.\n"
169
+ '3. Tag attributes (name="value") must remain in English.\n'
170
+ "4. Only translate the TEXT CONTENT between tags.\n"
171
+ "5. Preserve ALL tags exactly as they appear in the input.\n"
172
+ "6. DO NOT add display_name attribute if it is missing.\n"
173
+ "7. Examples of what NOT to translate:\n"
174
+ " - <video>, <problem>, <html>, <div>, <p>, etc.\n"
175
+ " - Attributes: url_name, filename, src, etc.\n"
176
+ " - Self-closing tags: <vertical />, <sequential />\n\n"
177
+ "GENERAL TRANSLATION RULES:\n"
178
+ "1. Output ONLY the translation between the markers.\n"
179
+ "2. Maintain the original formatting and structure.\n"
180
+ "3. Keep proper nouns, brand names, and acronyms unchanged.\n"
181
+ "4. Do NOT include explanations, notes, or commentary.\n"
182
+ "5. Preserve spacing, line breaks, and indentation.\n"
183
+ )
184
+
185
+ if glossary_directory:
186
+ glossary_terms = load_glossary(target_language, glossary_directory)
187
+ if glossary_terms:
188
+ system_prompt += (
189
+ f"\nGLOSSARY TERMS (use these translations):\n{glossary_terms}\n"
190
+ )
191
+
192
+ return system_prompt
193
+
194
+ def _parse_structured_response(
195
+ self, llm_response_text: str, original_subtitle_batch: list[srt.Subtitle]
196
+ ) -> list[srt.Subtitle]:
197
+ """
198
+ Parse the structured response and map back to original blocks.
199
+
200
+ Extracts translated content from LLM response using ID markers and maps
201
+ back to original subtitle objects preserving timestamps.
202
+
203
+ Args:
204
+ llm_response_text: Raw response text from LLM
205
+ original_subtitle_batch: Original subtitle batch for reference
206
+
207
+ Returns:
208
+ List of parsed subtitle objects with translations
209
+ """
210
+ parsed_subtitle_list = []
211
+
212
+ subtitle_id_pattern = re.compile(
213
+ r":::(\d+):::\s*(.*?)(?=(?::::\d+:::|$))", re.DOTALL
214
+ )
215
+ subtitle_matches = subtitle_id_pattern.findall(llm_response_text)
216
+
217
+ translation_map = {}
218
+ for subtitle_id_str, translated_text in subtitle_matches:
219
+ clean_subtitle_id = subtitle_id_str.strip()
220
+ translation_map[clean_subtitle_id] = translated_text.strip()
221
+
222
+ for original_subtitle in original_subtitle_batch:
223
+ subtitle_id_key = str(original_subtitle.index)
224
+ if subtitle_id_key in translation_map:
225
+ parsed_subtitle_list.append(
226
+ srt.Subtitle(
227
+ index=original_subtitle.index,
228
+ start=original_subtitle.start,
229
+ end=original_subtitle.end,
230
+ content=translation_map[subtitle_id_key],
231
+ )
232
+ )
233
+ else:
234
+ logger.warning(
235
+ "Block %s missing in translation response. Leaving empty.",
236
+ subtitle_id_key,
237
+ )
238
+ parsed_subtitle_list.append(
239
+ srt.Subtitle(
240
+ index=original_subtitle.index,
241
+ start=original_subtitle.start,
242
+ end=original_subtitle.end,
243
+ content="",
244
+ )
245
+ )
246
+
247
+ return parsed_subtitle_list
248
+
249
+ def _parse_text_response(self, llm_response_text: str) -> str:
250
+ """
251
+ Parse the structured text translation response.
252
+
253
+ Extracts translated content between translation markers, filtering out
254
+ explanations and metadata.
255
+
256
+ Args:
257
+ llm_response_text: Raw response text from LLM
258
+
259
+ Returns:
260
+ Cleaned translated text
261
+ """
262
+ # Try to extract content between markers
263
+ start_idx = llm_response_text.find(TRANSLATION_MARKER_START)
264
+ end_idx = llm_response_text.find(TRANSLATION_MARKER_END)
265
+
266
+ if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
267
+ # Extract content between markers
268
+ translated_content = llm_response_text[
269
+ start_idx + len(TRANSLATION_MARKER_START) : end_idx
270
+ ]
271
+ return translated_content.strip()
272
+
273
+ # Fallback: if markers not found, try to extract without explanation
274
+ lines = llm_response_text.split("\n")
275
+ filtered_lines = []
276
+ skip_explanation = False
277
+
278
+ for line in lines:
279
+ lower_line = line.lower().strip()
280
+ # Skip lines that look like explanations
281
+ if any(phrase in lower_line for phrase in LLM_EXPLANATION_KEYWORDS):
282
+ skip_explanation = True
283
+ continue
284
+
285
+ # If we hit the translation markers in any form, start including
286
+ if TRANSLATION_MARKER_START.lower() in lower_line:
287
+ skip_explanation = False
288
+ continue
289
+
290
+ if TRANSLATION_MARKER_END.lower() in lower_line:
291
+ break
292
+
293
+ if not skip_explanation and line.strip():
294
+ filtered_lines.append(line)
295
+
296
+ result = "\n".join(filtered_lines).strip()
297
+
298
+ # If we still have no result, return the original response
299
+ return result if result else llm_response_text.strip()
300
+
301
+ def _call_llm(
302
+ self, system_prompt: str, user_content: str, **additional_kwargs: Any
303
+ ) -> str:
304
+ """
305
+ Call the LLM API with system and user prompts.
306
+
307
+ Args:
308
+ system_prompt: System prompt defining LLM behavior
309
+ user_content: User content to translate
310
+ **additional_kwargs: Additional arguments for the API call
311
+
312
+ Returns:
313
+ LLM response content as string
314
+ """
315
+ llm_messages = [
316
+ {"role": "system", "content": system_prompt},
317
+ {"role": "user", "content": user_content},
318
+ ]
319
+ llm_response = completion(
320
+ model=self.model_name,
321
+ messages=llm_messages,
322
+ api_key=self.primary_api_key,
323
+ timeout=self.timeout,
324
+ **additional_kwargs,
325
+ )
326
+ return llm_response.choices[0].message.content.strip()
327
+
328
+ def translate_subtitles(
329
+ self,
330
+ subtitle_list: list[srt.Subtitle],
331
+ target_language: str,
332
+ glossary_directory: str | None = None,
333
+ ) -> list[srt.Subtitle]:
334
+ """
335
+ Translate subtitles using LLM.
336
+
337
+ Processes subtitles in batches with dynamic batch sizing to handle API limits.
338
+
339
+ Args:
340
+ subtitle_list: List of subtitle objects to translate
341
+ target_language: Target language code
342
+ glossary_directory: Path to glossary directory (optional)
343
+
344
+ Returns:
345
+ List of translated subtitle objects
346
+ """
347
+ system_prompt = self._get_subtitle_system_prompt(
348
+ target_language, glossary_directory
349
+ )
350
+
351
+ translated_subtitle_list = []
352
+ current_batch_size = len(subtitle_list)
353
+
354
+ current_index = 0
355
+ retry_count = 0
356
+ max_retries = 3
357
+
358
+ while current_index < len(subtitle_list):
359
+ subtitle_batch = subtitle_list[
360
+ current_index : current_index + current_batch_size
361
+ ]
362
+
363
+ user_payload_parts = []
364
+ for subtitle_item in subtitle_batch:
365
+ user_payload_parts.append(f":::{subtitle_item.index}:::")
366
+ user_payload_parts.append(subtitle_item.content)
367
+ user_payload_parts.append("")
368
+ user_payload = "\n".join(user_payload_parts)
369
+
370
+ logger.info(
371
+ " Translating batch starting at ID %s (%s blocks)...",
372
+ subtitle_batch[0].index,
373
+ len(subtitle_batch),
374
+ )
375
+
376
+ try:
377
+ llm_response_text = self._call_llm(system_prompt, user_payload)
378
+ translated_batch = self._parse_structured_response(
379
+ llm_response_text, subtitle_batch
380
+ )
381
+ translated_subtitle_list.extend(translated_batch)
382
+ current_index += current_batch_size
383
+ retry_count = 0 # Reset retry count on success
384
+
385
+ except Exception as llm_error:
386
+ error_message = str(llm_error).lower()
387
+ if any(
388
+ error_term in error_message for error_term in LLM_ERROR_KEYWORDS
389
+ ):
390
+ if current_batch_size <= 1 or retry_count >= max_retries:
391
+ logger.exception(
392
+ "Failed after %s batch size reduction attempts", retry_count
393
+ )
394
+ raise
395
+
396
+ logger.warning(
397
+ "Error: %s. Reducing batch size (attempt %s/%s)...",
398
+ llm_error,
399
+ retry_count + 1,
400
+ max_retries,
401
+ )
402
+ current_batch_size = max(1, current_batch_size // 2)
403
+ retry_count += 1
404
+ continue
405
+ else:
406
+ raise
407
+
408
+ return translated_subtitle_list
409
+
410
+ def translate_text(
411
+ self,
412
+ source_text: str,
413
+ target_language: str,
414
+ tag_handling: str | None = None, # noqa: ARG002
415
+ glossary_directory: str | None = None,
416
+ ) -> str:
417
+ """
418
+ Translate text using LLM.
419
+
420
+ Handles plain text, HTML, and XML content with appropriate prompting.
421
+
422
+ Args:
423
+ source_text: Text to translate
424
+ target_language: Target language code
425
+ tag_handling: How to handle XML/HTML tags (not used for LLM)
426
+ glossary_directory: Path to glossary directory (optional)
427
+
428
+ Returns:
429
+ Translated text
430
+ """
431
+ if not source_text or not source_text.strip():
432
+ return source_text
433
+
434
+ system_prompt = self._get_text_system_prompt(
435
+ target_language, glossary_directory
436
+ )
437
+
438
+ try:
439
+ llm_response = self._call_llm(system_prompt, source_text)
440
+ logger.info(
441
+ "\n\n\nSource Text:\n%s\n LLM Response:\n%s\n\n",
442
+ source_text,
443
+ llm_response,
444
+ )
445
+ return self._parse_text_response(llm_response)
446
+ except (ValueError, ConnectionError) as llm_error:
447
+ logger.warning("LLM translation failed: %s", llm_error)
448
+ return source_text
449
+
450
+ def translate_document(
451
+ self,
452
+ input_file_path: Path,
453
+ output_file_path: Path,
454
+ source_language: str, # noqa: ARG002
455
+ target_language: str,
456
+ glossary_directory: str | None = None,
457
+ ) -> None:
458
+ """
459
+ Translate document by reading and translating content.
460
+
461
+ Handles SRT files using subtitle translation and other files as text.
462
+
463
+ Args:
464
+ input_file_path: Path to input file
465
+ output_file_path: Path to output file
466
+ source_language: Source language code (not used)
467
+ target_language: Target language code
468
+ glossary_directory: Path to glossary directory (optional)
469
+ """
470
+ # For SRT files, use subtitle translation
471
+ if input_file_path.suffix == ".srt":
472
+ srt_content = input_file_path.read_text(encoding="utf-8")
473
+ subtitle_list = list(srt.parse(srt_content))
474
+
475
+ translated_subtitle_list = self.translate_srt_with_validation(
476
+ subtitle_list, target_language, glossary_directory
477
+ )
478
+
479
+ translated_srt_content = srt.compose(translated_subtitle_list)
480
+ output_file_path.write_text(translated_srt_content, encoding="utf-8")
481
+ else:
482
+ # For other files, treat as text
483
+ file_content = input_file_path.read_text(encoding="utf-8")
484
+ translated_file_content = self.translate_text(
485
+ file_content, target_language, glossary_directory=glossary_directory
486
+ )
487
+ output_file_path.write_text(translated_file_content, encoding="utf-8")
488
+
489
+
490
+ class OpenAIProvider(LLMProvider):
491
+ """OpenAI translation provider."""
492
+
493
+ def __init__(
494
+ self,
495
+ primary_api_key: str,
496
+ repair_api_key: str | None = None,
497
+ model_name: str | None = None,
498
+ ):
499
+ """
500
+ Initialize OpenAI provider.
501
+
502
+ Args:
503
+ primary_api_key: OpenAI API key
504
+ repair_api_key: API key for repair service (optional)
505
+ model_name: OpenAI model name (e.g., "gpt-5.2")
506
+
507
+ Raises:
508
+ ValueError: If model_name is not provided
509
+ """
510
+ if not model_name:
511
+ msg = "model_name is required for OpenAIProvider"
512
+ raise ValueError(msg)
513
+ super().__init__(primary_api_key, repair_api_key, f"openai/{model_name}")
514
+
515
+ def _call_llm(
516
+ self, system_prompt: str, user_content: str, **additional_kwargs: Any
517
+ ) -> str:
518
+ """
519
+ Call OpenAI API with prompts.
520
+
521
+ Args:
522
+ system_prompt: System prompt defining behavior
523
+ user_content: User content to translate
524
+ **additional_kwargs: Additional arguments for the API
525
+
526
+ Returns:
527
+ OpenAI response content
528
+ """
529
+ return super()._call_llm(system_prompt, user_content, **additional_kwargs)
530
+
531
+
532
+ class GeminiProvider(LLMProvider):
533
+ """Gemini translation provider."""
534
+
535
+ def __init__(
536
+ self,
537
+ primary_api_key: str,
538
+ repair_api_key: str | None = None,
539
+ model_name: str | None = None,
540
+ ):
541
+ """
542
+ Initialize Gemini provider.
543
+
544
+ Args:
545
+ primary_api_key: Gemini API key
546
+ repair_api_key: API key for repair service (optional)
547
+ model_name: Gemini model name (e.g., "gemini-3-pro-preview")
548
+
549
+ Raises:
550
+ ValueError: If model_name is not provided
551
+ """
552
+ if not model_name:
553
+ msg = "model_name is required for GeminiProvider"
554
+ raise ValueError(msg)
555
+ super().__init__(primary_api_key, repair_api_key, f"gemini/{model_name}")
556
+
557
+
558
+ class MistralProvider(LLMProvider):
559
+ """Mistral translation provider."""
560
+
561
+ def __init__(
562
+ self,
563
+ primary_api_key: str,
564
+ repair_api_key: str | None = None,
565
+ model_name: str | None = None,
566
+ ):
567
+ """
568
+ Initialize Mistral provider.
569
+
570
+ Args:
571
+ primary_api_key: Mistral API key
572
+ repair_api_key: API key for repair service (optional)
573
+ model_name: Mistral model name (e.g., "mistral-large-latest")
574
+
575
+ Raises:
576
+ ValueError: If model_name is not provided
577
+ """
578
+ if not model_name:
579
+ msg = "model_name is required for MistralProvider"
580
+ raise ValueError(msg)
581
+ super().__init__(primary_api_key, repair_api_key, f"mistral/{model_name}")
@@ -0,0 +1,17 @@
1
+ # noqa: INP001
2
+
3
+ """Settings to provide to edX"""
4
+
5
+ from ol_openedx_course_translations.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_course_translations.middleware.CourseLanguageCookieResetMiddleware"
16
+ ]
17
+ )
@@ -1,37 +1,65 @@
1
1
  # noqa: INP001
2
2
 
3
- """Settings to provide to edX"""
3
+ """Common settings for LMS and CMS to provide to edX"""
4
4
 
5
5
 
6
- def plugin_settings(settings):
6
+ def apply_common_settings(settings):
7
7
  """
8
- Populate common settings
8
+ Apply custom settings function for LMS and CMS settings.
9
+
10
+ Configures translation-related settings including language selection,
11
+ supported file types, translation providers, and repository settings.
12
+
13
+ Args:
14
+ settings: Django settings object to modify
9
15
  """
10
- env_tokens = getattr(settings, "ENV_TOKENS", {})
11
- settings.DEEPL_API_KEY = env_tokens.get("DEEPL_API_KEY", "")
12
- settings.OL_OPENEDX_COURSE_TRANSLATIONS_TARGET_DIRECTORIES = env_tokens.get(
13
- "OL_OPENEDX_COURSE_TRANSLATIONS_TARGET_DIRECTORIES",
14
- [
15
- "about",
16
- "course",
17
- "chapter",
18
- "html",
19
- "info",
20
- "problem",
21
- "sequential",
22
- "vertical",
23
- "video",
24
- "static",
25
- "tabs",
26
- ],
27
- )
28
- settings.OL_OPENEDX_COURSE_TRANSLATIONS_SUPPORTED_ARCHIVE_EXTENSIONS = (
29
- env_tokens.get(
30
- "OL_OPENEDX_COURSE_TRANSLATIONS_SUPPORTED_ARCHIVE_EXTENSIONS",
31
- [".tar.gz", ".tgz", ".tar"],
32
- )
33
- )
34
- settings.OL_OPENEDX_COURSE_TRANSLATIONS_TRANSLATABLE_EXTENSIONS = env_tokens.get(
35
- "OL_OPENEDX_COURSE_TRANSLATIONS_TRANSLATABLE_EXTENSIONS",
36
- [".html", ".xml"],
16
+ settings.ENABLE_AUTO_LANGUAGE_SELECTION = False
17
+ settings.AUTO_LANGUAGE_SELECTION_EXEMPT_PATHS = ["admin", "sysadmin", "instructor"]
18
+ settings.COURSE_TRANSLATIONS_TARGET_DIRECTORIES = [
19
+ "about",
20
+ "course",
21
+ "chapter",
22
+ "html",
23
+ "info",
24
+ "problem",
25
+ "sequential",
26
+ "vertical",
27
+ "video",
28
+ "static",
29
+ "tabs",
30
+ ]
31
+ settings.COURSE_TRANSLATIONS_SUPPORTED_ARCHIVE_EXTENSIONS = [
32
+ ".tar.gz",
33
+ ".tgz",
34
+ ".tar",
35
+ ]
36
+ settings.COURSE_TRANSLATIONS_TRANSLATABLE_EXTENSIONS = [
37
+ ".html",
38
+ ".xml",
39
+ ".srt",
40
+ ]
41
+ settings.TRANSLATIONS_PROVIDERS = {
42
+ "default_provider": "mistral",
43
+ "deepl": {
44
+ "api_key": "",
45
+ },
46
+ "openai": {
47
+ "api_key": "",
48
+ "default_model": "gpt-5.2",
49
+ },
50
+ "gemini": {
51
+ "api_key": "",
52
+ "default_model": "gemini-3-pro-preview",
53
+ },
54
+ "mistral": {
55
+ "api_key": "",
56
+ "default_model": "mistral-large-latest",
57
+ },
58
+ }
59
+ settings.TRANSLATIONS_GITHUB_TOKEN = ""
60
+ # Translation repository settings
61
+ settings.TRANSLATIONS_REPO_PATH = ""
62
+ settings.TRANSLATIONS_REPO_URL = (
63
+ "https://github.com/mitodl/mitxonline-translations.git"
37
64
  )
65
+ settings.LITE_LLM_REQUEST_TIMEOUT = 120 # seconds