-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: sync from old transifex project
This is a on-demand GitHub Actions workflow which will sync traslations and their status from the `open-edx/edx-platform` Transifex project (old) into `open-edx/openedx-translations` OEP-58 project (new).
- Loading branch information
1 parent
222bf14
commit 0467f07
Showing
4 changed files
with
316 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
name: Migrate translations from the old Transifex project | ||
|
||
on: | ||
# schedule: # TODO: Maybe before merge | ||
# # https://crontab.guru/#0_0_*_*_1 | ||
# # At midnight on Mondays | ||
# - cron: 0 0 * * 1" | ||
workflow_dispatch: | ||
# push: | ||
# branches: [sync_translations] # TODO: Remove before merge | ||
|
||
jobs: | ||
migrate-translations: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
batch: | ||
- new_slug: edx-ora2 | ||
old_slug: openassessment | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: edx-ora2-js | ||
old_slug: openassessment-js | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: edx-proctoring | ||
old_slug: edx-proctoring | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: studio-frontend | ||
old_slug: studio-frontend | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: donexblock | ||
old_slug: xblock-done | ||
old_project_slug: xblocks | ||
|
||
- new_slug: drag-and-drop-v2 | ||
old_slug: drag-and-drop-v2 | ||
old_project_slug: xblocks | ||
|
||
- new_slug: xblock-free-text-response | ||
old_slug: xblock-free-text-response | ||
old_project_slug: xblocks | ||
|
||
- new_slug: course-discovery | ||
old_slug: course_discovery | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: course-discovery-js | ||
old_slug: course_discovery | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: credentials-js | ||
old_slug: credentials | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-account | ||
old_slug: frontend-app-account | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-authn | ||
old_slug: frontend-app-authn | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-course-authoring | ||
old_slug: frontend-app-course-authoring | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-discussions | ||
old_slug: frontend-app-discussions | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-ecommerce | ||
old_slug: frontend-app-ecommerce | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-gradebook | ||
old_slug: frontend-app-gradebook | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-learner-dashboard | ||
old_slug: frontend-app-learner-dashboard | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-learner-record | ||
old_slug: frontend-app-learner-record | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-learning | ||
old_slug: frontend-app-learning | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-profile | ||
old_slug: frontend-app-profile | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-app-program-console | ||
old_slug: frontend-app-program-manager | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-component-footer | ||
old_slug: frontend-component-footer-edx | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: frontend-component-header | ||
old_slug: frontend-component-header | ||
old_project_slug: edx-platform | ||
|
||
- new_slug: paragon | ||
old_slug: paragon | ||
old_project_slug: edx-platform | ||
|
||
|
||
# if: ${{ !github.event.inputs.resource || github.event.inputs.resource == matrix.new_resource_slug }} | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: setup python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.8 | ||
- name: Install Python dependencies | ||
run: make sync_requirements | ||
|
||
- name: Sync | ||
env: | ||
# `TX_LANGUAGES` list of languages is set in the `Makefile` | ||
TX_NEW_SLUG: ${{ matrix.batch.new_slug }} | ||
TX_OLD_SLUG: ${{ matrix.batch.old_slug }} | ||
TX_OLD_PROJECT_SLUG: ${{ matrix.batch.old_project_slug }} | ||
TX_API_TOKEN: ${{ secrets.TRANSIFEX_API_TOKEN }} | ||
run: make sync_translations |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import configparser | ||
import os | ||
import sys | ||
from os.path import expanduser | ||
import yaml | ||
|
||
from transifex.api import transifex_api | ||
|
||
NEW_PROJECT_SLUG = 'openedx-translations' | ||
ORGANIZATION_SLUG = 'open-edx' | ||
|
||
|
||
class Command: | ||
|
||
workflow_file_path = '.github/workflows/sync-translations.yml' | ||
|
||
def __init__(self, argv, tx_api, environ): | ||
self.argv = argv | ||
self.tx_api = tx_api | ||
self.environ = environ | ||
|
||
def is_dry_run(self): | ||
""" | ||
Check if the script is running in dry-run mode. | ||
""" | ||
return '--dry-run' in self.argv | ||
|
||
def is_simulated_github_actions(self): | ||
""" | ||
Check if the script is running in simulated GitHub Actions mode. | ||
""" | ||
return '--simulate-github-workflow' in self.argv | ||
|
||
def get_resource_url(self, resource, project_slug): | ||
return f'https://www.transifex.com/{ORGANIZATION_SLUG}/{project_slug}/{resource.slug}' | ||
|
||
def get_transifex_organization_projects(self): | ||
""" | ||
Get openedx-translations project from Transifex. | ||
""" | ||
tx_api_token = self.environ.get('TX_API_TOKEN') | ||
if not tx_api_token: | ||
config = configparser.ConfigParser() | ||
config.read(expanduser('~/.transifexrc')) | ||
tx_api_token = config['https://www.transifex.com']['password'] | ||
|
||
if not tx_api_token: | ||
raise Exception( | ||
'Error: No auth token found. ' | ||
'Set transifex API token via TX_API_TOKEN environment variable or via the ~/.transifexrc file.' | ||
) | ||
|
||
self.tx_api.setup(auth=tx_api_token) | ||
return self.tx_api.Organization.get(slug=ORGANIZATION_SLUG).fetch('projects') | ||
|
||
def get_resources_pair(self, new_slug, old_slug, old_project_slug): | ||
""" | ||
Load the old and new Transifex resources pair. | ||
""" | ||
projects = self.get_transifex_organization_projects() | ||
new_project = projects.get(slug=NEW_PROJECT_SLUG) | ||
|
||
new_resource_id = f'o:{ORGANIZATION_SLUG}:p:{new_project.slug}:r:{new_slug}' | ||
new_resource = self.tx_api.Resource.get(id=new_resource_id) | ||
|
||
print(new_resource, new_resource.name, new_resource.id) | ||
|
||
pair_id = f'o:{ORGANIZATION_SLUG}:p:{old_project_slug}:r:{old_slug}' | ||
old_resource = self.tx_api.Resource.get(id=pair_id) | ||
print(old_resource, old_resource.name) | ||
|
||
print(f'Syncing {new_resource.name} from {old_resource.name}...') | ||
return { | ||
'old_resource': old_resource, | ||
'new_resource': new_resource, | ||
} | ||
|
||
def get_translations(self, language_code, resource): | ||
""" | ||
Get a list of translations for a given language and resource. | ||
""" | ||
language = self.tx_api.Language.get(code=language_code) | ||
translations = self.tx_api.ResourceTranslation. \ | ||
filter(resource=resource, language=language). \ | ||
include('resource_string') | ||
|
||
return translations.all() | ||
|
||
def sync_translations(self, language_code, old_resource, new_resource): | ||
""" | ||
Sync specific language translations into the new Transifex resource. | ||
""" | ||
print(' syncing', language_code, '...') | ||
old_translations = { | ||
self.get_translation_id(translation): translation | ||
for translation in self.get_translations(language_code=language_code, resource=old_resource) | ||
} | ||
|
||
for new_translation in self.get_translations(language_code=language_code, resource=new_resource): | ||
translation_id = self.get_translation_id(new_translation) | ||
if old_translation := old_translations.get(translation_id): | ||
updates = {} | ||
for attr in ['reviewed', 'proofread', 'strings', 'tags']: | ||
if old_attr_value := getattr(old_translation, attr, None): | ||
if old_attr_value != getattr(new_translation, attr, None): | ||
updates[attr] = old_attr_value | ||
|
||
if updates: | ||
print(translation_id, updates) | ||
|
||
if not self.is_dry_run(): | ||
new_translation.save(**updates) | ||
|
||
def get_translation_id(self, translation): | ||
""" | ||
Build a unique identifier for a translation entry. | ||
""" | ||
return f'context:{translation.resource_string.context}:key:{translation.resource_string.key}' | ||
|
||
def get_languages(self): | ||
""" | ||
Get a list of languages to sync translations for. | ||
""" | ||
return self.environ['TX_LANGUAGES'].split(',') | ||
|
||
def sync_pair_into_new_resource(self, new_slug, old_slug, old_project_slug): | ||
""" | ||
Sync translations from both the edx-platform and XBlock projects into the new openedx-translations project. | ||
""" | ||
languages = self.get_languages() | ||
resource_pair = self.get_resources_pair(new_slug, old_slug, old_project_slug) | ||
|
||
print(f'Syncing: {languages}') | ||
print(f' - from: {self.get_resource_url(resource_pair["old_resource"], old_project_slug)}') | ||
print(f' - to: {self.get_resource_url(resource_pair["new_resource"], NEW_PROJECT_SLUG)}') | ||
|
||
for lang_code in languages: | ||
self.sync_translations(language_code=lang_code, **resource_pair) | ||
|
||
def run_from_workflow_yaml_file(self, workflow_configs): | ||
""" | ||
Run the script from a GitHub Actions migrate-from-transifex-old-project.yml workflow file. | ||
""" | ||
pairs_list = workflow_configs['jobs']['migrate-translations']['strategy']['matrix']['batch'] | ||
|
||
for pair in pairs_list: | ||
self.sync_pair_into_new_resource( | ||
new_slug=pair['new_slug'], | ||
old_slug=pair['old_slug'], | ||
old_project_slug=pair['old_project_slug'], | ||
) | ||
|
||
def run(self): | ||
if self.is_simulated_github_actions(): | ||
with open(self.workflow_file_path) as workflow_file: | ||
self.run_from_workflow_yaml_file( | ||
workflow_configs=yaml.safe_load(workflow_file.read()), | ||
) | ||
else: | ||
self.sync_pair_into_new_resource( | ||
new_slug=self.environ['TX_NEW_SLUG'], | ||
old_slug=self.environ['TX_OLD_SLUG'], | ||
old_project_slug=self.environ['TX_OLD_PROJECT_SLUG'], | ||
) | ||
|
||
|
||
if __name__ == '__main__': | ||
command = Command(sys.argv, environ=os.environ, tx_api=transifex_api) | ||
command.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters