Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/ol_openedx_ai_static_translations/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Change Log
----------

..
All enhancements and patches to ol_openedx_ai_static_translations will be documented
in this file. It adheres to the structure of https://keepachangelog.com/ ,
but in reStructuredText instead of Markdown (for ease of incorporation into
Sphinx documentation and the PyPI description).
This project adheres to Semantic Versioning (https://semver.org/).
.. There should always be an "Unreleased" section for changes pending release.
28 changes: 28 additions & 0 deletions src/ol_openedx_ai_static_translations/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (C) 2022 MIT Open Learning

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
54 changes: 54 additions & 0 deletions src/ol_openedx_ai_static_translations/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
OL Open edX AI Static Translations
====================================

An Open edX plugin that provides AI-powered static translation management. It syncs translation keys, translates them using LLM providers, and creates pull requests with translated content.

Purpose
*******

This plugin provides the ``sync_and_translate_language`` management command for syncing and translating Open edX static strings (frontend JSON and backend PO files) using LLM providers (OpenAI, Gemini, Mistral) with optional glossary support.

Setup
=====

For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.

Installation required in:

* Studio (CMS)

Configuration
=============

This plugin shares settings with ``ol_openedx_course_translations``. Ensure the following settings are configured:

.. code-block:: python

TRANSLATIONS_PROVIDERS: {
"default_provider": "mistral",
"openai": {"api_key": "", "default_model": "gpt-5.2"},
"gemini": {"api_key": "", "default_model": "gemini-3-pro-preview"},
"mistral": {"api_key": "", "default_model": "mistral-large-latest"},
}
TRANSLATIONS_GITHUB_TOKEN: <YOUR_GITHUB_TOKEN>
TRANSLATIONS_REPO_PATH: ""
TRANSLATIONS_REPO_URL: "https://github.com/mitodl/mitxonline-translations.git"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two settings are confusing, there should be a documentation on which setting does what.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added inline documentation in 020fd79:

  • TRANSLATIONS_GITHUB_TOKEN — personal access token with repo write permissions for creating PRs
  • TRANSLATIONS_REPO_PATH — local filesystem path where the translations repo will be cloned/checked out
  • TRANSLATIONS_REPO_URL — URL of the remote translations repository


Usage
=====

.. code-block:: bash

# Sync and translate a language
./manage.py cms sync_and_translate_language el

# With specific provider and model
./manage.py cms sync_and_translate_language el --provider openai --model gpt-5.2 --glossary /path/to/glossary

License
*******

The code in this repository is licensed under the BSD 3-Clause license unless
otherwise noted.

Please see `LICENSE.txt <LICENSE.txt>`_ for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
MIT's Open edX AI static translations plugin
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
ol_openedx_ai_static_translations Django application initialization.
"""

from django.apps import AppConfig
from edx_django_utils.plugins import PluginSettings
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType


class OLOpenedXAIStaticTranslationsConfig(AppConfig):
"""
Configuration for the ol_openedx_ai_static_translations Django application.
"""

name = "ol_openedx_ai_static_translations"
verbose_name = "OL AI Static Translations"

plugin_app = {
PluginSettings.CONFIG: {
ProjectType.CMS: {
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.cms"},
},
ProjectType.LMS: {
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.lms"},
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
"""Constants for AI static translation synchronization."""

# LLM Provider names
PROVIDER_DEEPL = "deepl"
PROVIDER_GEMINI = "gemini"
PROVIDER_MISTRAL = "mistral"
PROVIDER_OPENAI = "openai"

# Learner-facing frontend applications that require translation
LEARNER_FACING_APPS = [
"frontend-app-learning",
"frontend-app-learner-dashboard",
"frontend-app-learner-record",
"frontend-app-account",
"frontend-app-profile",
"frontend-app-authn",
"frontend-app-catalog",
"frontend-app-discussions",
"frontend-component-header",
"frontend-component-footer",
"frontend-app-ora",
"frontend-platform",
]

# Plural forms configuration for different languages
# Based on GNU gettext plural forms specification
# See: https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
PLURAL_FORMS = {
# Languages with no plural forms (nplurals=1)
"ja": "nplurals=1; plural=0;", # Japanese
"ko": "nplurals=1; plural=0;", # Korean
"zh": "nplurals=1; plural=0;", # Chinese (all variants)
"th": "nplurals=1; plural=0;", # Thai
"vi": "nplurals=1; plural=0;", # Vietnamese
"id": "nplurals=1; plural=0;", # Indonesian
"ms": "nplurals=1; plural=0;", # Malay
"km": "nplurals=1; plural=0;", # Khmer
"bo": "nplurals=1; plural=0;", # Tibetan
# Languages with 2 plural forms: plural=(n != 1)
"en": "nplurals=2; plural=(n != 1);", # English
"es": "nplurals=2; plural=(n != 1);", # Spanish (all variants)
"de": "nplurals=2; plural=(n != 1);", # German
"el": "nplurals=2; plural=(n != 1);", # Greek
"it": "nplurals=2; plural=(n != 1);", # Italian
"pt": "nplurals=2; plural=(n != 1);", # Portuguese (all variants)
"nl": "nplurals=2; plural=(n != 1);", # Dutch
"sv": "nplurals=2; plural=(n != 1);", # Swedish
"da": "nplurals=2; plural=(n != 1);", # Danish
"no": "nplurals=2; plural=(n != 1);", # Norwegian
"nb": "nplurals=2; plural=(n != 1);", # Norwegian Bokmål
"nn": "nplurals=2; plural=(n != 1);", # Norwegian Nynorsk
"fi": "nplurals=2; plural=(n != 1);", # Finnish
"is": "nplurals=2; plural=(n != 1);", # Icelandic
"et": "nplurals=2; plural=(n != 1);", # Estonian
"lv": "nplurals=2; plural=(n != 1);", # Latvian
"he": "nplurals=2; plural=(n != 1);", # Hebrew
"hi": "nplurals=2; plural=(n != 1);", # Hindi
"bn": "nplurals=2; plural=(n != 1);", # Bengali
"gu": "nplurals=2; plural=(n != 1);", # Gujarati
"kn": "nplurals=2; plural=(n != 1);", # Kannada
"ml": "nplurals=2; plural=(n != 1);", # Malayalam
"ta": "nplurals=2; plural=(n != 1);", # Tamil
"te": "nplurals=2; plural=(n != 1);", # Telugu
"or": "nplurals=2; plural=(n != 1);", # Oriya
"si": "nplurals=2; plural=(n != 1);", # Sinhala
"ne": "nplurals=2; plural=(n != 1);", # Nepali
"mr": "nplurals=2; plural=(n != 1);", # Marathi
"ur": "nplurals=2; plural=(n != 1);", # Urdu
"az": "nplurals=2; plural=(n != 1);", # Azerbaijani
"uz": "nplurals=2; plural=(n != 1);", # Uzbek
"kk": "nplurals=2; plural=(n != 1);", # Kazakh
"mn": "nplurals=2; plural=(n != 1);", # Mongolian
"sq": "nplurals=2; plural=(n != 1);", # Albanian
"eu": "nplurals=2; plural=(n != 1);", # Basque
"ca": "nplurals=2; plural=(n != 1);", # Catalan
"gl": "nplurals=2; plural=(n != 1);", # Galician
"tr": "nplurals=2; plural=(n != 1);", # Turkish
"af": "nplurals=2; plural=(n != 1);", # Afrikaans
"fil": "nplurals=2; plural=(n != 1);", # Filipino
# Languages with 2 plural forms: plural=(n > 1)
"fr": "nplurals=2; plural=(n > 1);", # French
"br": "nplurals=2; plural=(n > 1);", # Breton
# Languages with 3 plural forms
"pl": (
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Polish
"ru": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Russian
"uk": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Ukrainian
"be": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Belarusian
"sr": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Serbian
"hr": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Croatian
"bs": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Bosnian
"cs": "nplurals=3; plural=(n==1 ? 0 : (n>=2 && n<=4) ? 1 : 2);", # Czech
"sk": "nplurals=3; plural=(n==1 ? 0 : (n>=2 && n<=4) ? 1 : 2);", # Slovak
"lt": (
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);"
), # Lithuanian
"hy": "nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);", # Armenian
"ro": (
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);"
), # Romanian
# Languages with 4 plural forms
"cy": (
"nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n==8 || n==11) ? 2 : 3);"
), # Welsh
"ga": "nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 : 3);", # Irish
"gd": (
"nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : "
"(n>2 && n<20) ? 2 : 3);"
), # Scottish Gaelic
"mt": (
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>=2 && n%100<=10) ? 1 : "
"(n%100>=11 && n%100<=19) ? 2 : 3);"
), # Maltese
# Languages with 6 plural forms
"ar": (
"nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && "
"n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);"
), # Arabic
# Other languages
"fa": "nplurals=2; plural=(n==0 || n==1 ? 0 : 1);", # Persian/Farsi
"hu": "nplurals=2; plural=(n != 1);", # Hungarian
"bg": "nplurals=2; plural=(n != 1);", # Bulgarian
"am": "nplurals=2; plural=(n > 1);", # Amharic
}

# Default plural form fallback (English-style)
# Used when a language code is not found in PLURAL_FORMS
DEFAULT_PLURAL_FORM = "nplurals=2; plural=(n != 1);"

# Typo patterns to fix in translation files
TYPO_PATTERNS = [
("Serch", "Search"),
]

# Backend PO file names
BACKEND_PO_FILES = ["django.po", "djangojs.po"]

# Backend plugin apps: (repo_dir, module_name) under translations/.
# Used by sync_and_translate_language to sync/translate at
# translations/<repo_dir>/<module_name>/conf/locale/<lang>/LC_MESSAGES/django.po.
# When pulled in edx-platform (make pull_translations), these go to
# conf/plugins-locale/plugins/<module_name>/.
TRANSLATABLE_PLUGINS = [
("open-edx-plugins", "ol_openedx_chat"),
]

# PO file header metadata
PO_HEADER_PROJECT_VERSION = "0.1a"
PO_HEADER_BUGS_EMAIL = "openedx-translation@googlegroups.com"
PO_HEADER_POT_CREATION_DATE = "2023-06-13 08:00+0000"
PO_HEADER_MIME_VERSION = "1.0"
PO_HEADER_CONTENT_TYPE = "text/plain; charset=UTF-8"
PO_HEADER_CONTENT_TRANSFER_ENCODING = "8bit"
PO_HEADER_TRANSIFEX_TEAM_BASE_URL = "https://app.transifex.com/open-edx/teams/6205"

# File and directory names
TRANSLATION_FILE_NAMES = {
"transifex_input": "transifex_input.json",
"english": "en.json",
"messages_dir": "messages",
"i18n_dir": "i18n",
"locale_dir": "locale",
"lc_messages": "LC_MESSAGES",
"conf_dir": "conf",
"edx_platform": "edx-platform",
}

# JSON file formatting
DEFAULT_JSON_INDENT = 2

# Language code to human-readable name mapping
# Used in PO file headers for Language-Team field
LANGUAGE_MAPPING = {
"ar": "Arabic",
"de": "German",
"el": "Greek",
"es": "Spanish",
"fr": "French",
"hi": "Hindi",
"id": "Indonesian",
"ja": "Japanese",
"kr": "Korean",
"pt": "Portuguese",
"ru": "Russian",
"sq": "Albanian",
"tr": "Turkish",
"zh": "Chinese",
}

# Maximum number of retries for failed translation batches
MAX_RETRIES = 3

# Glossary parsing constants
EXPECTED_GLOSSARY_PARTS = 2 # English term and translation separated by "->"

# HTTP Status Codes
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_NOT_FOUND = 404
HTTP_TOO_MANY_REQUESTS = 429
HTTP_UNPROCESSABLE_ENTITY = 422

# Error message length limit
MAX_ERROR_MESSAGE_LENGTH = 200

# Maximum length for strings in log messages (truncate with "...")
MAX_LOG_STRING_LENGTH = 50
MAX_LOG_ICU_STRING_LENGTH = 100

# Plural category counts (GNU gettext nplurals)
PLURAL_CATEGORIES_ARABIC = 6 # zero, one, two, few, many, other
PLURAL_CATEGORIES_FOUR = 4 # one, two, few, other
PLURAL_CATEGORIES_THREE = 3 # one, few, other
PLURAL_CATEGORIES_TWO = 2 # one, other (most languages)
Comment on lines +225 to +235
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The LANGUAGE_MAPPING dictionary incorrectly uses "kr" for Korean instead of the standard ISO 639-1 code "ko", which is used elsewhere in the code.
Severity: LOW

Suggested Fix

In src/ol_openedx_ai_static_translations/ol_openedx_ai_static_translations/constants.py, change the key for the Korean language entry in the LANGUAGE_MAPPING dictionary from "kr" to "ko" to match the ISO 639-1 standard and align with other constants.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
src/ol_openedx_ai_static_translations/ol_openedx_ai_static_translations/constants.py#L1-L235

Potential issue: The `LANGUAGE_MAPPING` dictionary in `constants.py` defines Korean with
the language code `"kr"` instead of the correct ISO 639-1 code `"ko"`. Other parts of
the system, like `PLURAL_FORMS`, correctly use `"ko"`. When translating for Korean
(`ko`), lookups in `LANGUAGE_MAPPING` fail, causing fallback behavior. This results in
generated PO files having an incorrect `Language-Team` header (e.g., `"ko"` instead of
`"Korean"`) and LLM prompts being less clear (e.g., "Translate to ko" instead of
"Translate to Korean").

Loading
Loading