Skip to content

Commit

Permalink
feat: atlas push pull scripts: FC-55
Browse files Browse the repository at this point in the history
Extract will extract the English language resources from all
modules to the I18N folder.

Pull will pull all other languages translations from
the openedx-traslations repository to the
I18N folder then split them to thier modules.
  • Loading branch information
Amr-Nash committed May 24, 2024
1 parent 32bf7d9 commit 74c4769
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,10 @@ vendor/
venv/
Podfile.lock
config_settings.yaml
default_config/
default_config/

# Translations ignored files
.venv/
I18N/
*.lproj/
!en.lproj/
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
clean_translations_temp_directory:
rm -rf I18N/

translation_requirements:
pip install -r i18n_scripts/requirements.txt

pull_translations: clean_translations_temp_directory
atlas pull $(ATLAS_OPTIONS) translations/openedx-app-ios/I18N:I18N
python i18n_scripts/translation.py --split

extract_translations: clean_translations_temp_directory
python i18n_scripts/translation.py --combine
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,48 @@ Modern vision of the mobile application for the Open edX platform from Raccoon G

6. Click the **Run** button.

## Translations
### Getting translations for the app
Translations aren't included in the source code of this repository as of [OEP-58](https://docs.openedx.org/en/latest/developers/concepts/oep58.html). Therefore, they need to be pulled before testing or publishing to App Store.

To get the latest translations for all languages use the following command:
```bash
make pull_translations
```
This command runs [`atlas pull`](https://github.com/openedx/openedx-atlas) to download the latest translations files from the [openedx/openedx-translations](https://github.com/openedx/openedx-translations) repository. These files contain the latest translations for all languages. In the [openedx/openedx-translations](https://github.com/openedx/openedx-translations) repository each language's translations are saved as a single file e.g. `I18N/I18N/uk.lproj/Localization.strings` ([example](https://github.com/openedx/openedx-translations/blob/6448167e9695a921f003ff6bd8f40f006a2d6743/translations/openedx-app-ios/I18N/I18N/uk.lproj/Localizable.strings)). After these are pulled, each language's translation file is split into the App's modules e.g. `Discovery/Discovery/uk.lproj/Localization.strings`.

After this command is run the application can load the translations by changing the device (or the emulator) language in the settings.

### Using custom translations

By default, the command `make pull_translations` runs [`atlas pull`](https://github.com/openedx/openedx-atlas) with no arguments which pulls transaltions from the [openedx-translations repository](https://github.com/openedx/openedx-translations).

You can use custom translations on your fork of the openedx-translations repository by setting the following configuration parameters:

- `--revision` (default: `"main"`): Branch or git tag to pull translations from.
- `--repository` (default: `"openedx/openedx-translations"`): GitHub repository slug. There's a feature request to [support GitLab and other providers](https://github.com/openedx/openedx-atlas/issues/20).

Arguments can be passed via the `ATLAS_OPTIONS` environment variable as shown below:
``` bash
make ATLAS_OPTIONS='--repository=<your-github-org>/<repository-name> --revision=<branch-name>' pull_translations
```
Additional arguments can be passed to `atlas pull`. Refer to the [atlas documentations ](https://github.com/openedx/openedx-atlas) for more information.

### Testing translations

Until the [pull request #422](https://github.com/openedx/openedx-app-ios/pull/422) is merged, translations needs to be pulled from the testing branch `Zeit-Labs/openedx-translations` repo under `fc_55_sample` branch with the following options:
``` bash
make ATLAS_OPTIONS='--repository=Zeit-Labs/openedx-translations --revision=fc_55_sample' pull_translations
```
### How to translate the app

Translations are managed in the [open-edx/openedx-translations](https://app.transifex.com/open-edx/openedx-translations/dashboard/) Transifex project.

To translate the app join the [Transifex project](https://app.transifex.com/open-edx/openedx-translations/dashboard/) and add your translations `openedx-app-ios` resource: https://app.transifex.com/open-edx/openedx-translations/openedx-app-ios/ (the link will start working after the [pull request #442](https://github.com/openedx/openedx-app-ios/pull/422) is merged)

Once the resource is both 100% translated and reviewed the [Transifex integration](https://github.com/apps/transifex-integration) will automatically push it to the [openedx-translations](https://github.com/openedx/openedx-translations) repository and developers can use the translations in their app.


## API
This project targets on the latest Open edX release and rely on the relevant mobile APIs.

Expand Down
3 changes: 3 additions & 0 deletions i18n_scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Translation processing dependencies
openedx-atlas==0.6.0
localizable==0.1.3
273 changes: 273 additions & 0 deletions i18n_scripts/translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
This script performs two jobs:
1- Combine the English translations from all modules in the repository to the I18N directory. After the English
translation is combined, it will be pushed to the openedx-translations repository as described in OEP-58.
2- Split the pulled translation files from the openedx-translations repository into the iOS app modules.
More detailed specifications are described in the docs/0002-atlas-translations-management.rst design doc.
"""

import argparse
import os
import sys
from collections import defaultdict
import localizable


def parse_arguments():
"""
Parse command line arguments.
The script takes only one of the two arguments --split or --combine.
"""
parser = argparse.ArgumentParser(description='Split or combine translations.')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--split', action='store_true',
help='Split translations into separate files for each module and language.')
group.add_argument('--combine', action='store_true',
help='Combine the English translations from all modules into a single file.')
return parser.parse_args()


def get_translation_file_path(modules_dir, module_name, lang_dir, create_dirs=False):
"""
Retrieves the path of the translation file for a specified module and language directory.
Parameters:
modules_dir (str): The path to the base directory containing all the modules.
module_name (str): The name of the module for which the translation path is being retrieved.
lang_dir (str): The name of the language directory within the module's directory.
create_dirs (bool): If True, creates the parent directories if they do not exist. Defaults to False.
Returns:
str: The path to the module's translation file (Localizable.strings).
"""
try:
lang_dir_path = os.path.join(modules_dir, module_name, module_name, lang_dir, 'Localizable.strings')
if create_dirs:
os.makedirs(os.path.dirname(lang_dir_path), exist_ok=True)
return lang_dir_path
except Exception as e:
print(f"Error creating directory path: {e}", file=sys.stderr)
raise


def get_modules_to_translate(modules_dir):
"""
Retrieve the names of modules that have translation files for a specified language.
Parameters:
modules_dir (str): The path to the directory containing all the modules.
Returns:
list of str: A list of module names that have translation files for the specified language.
"""
try:
modules_list = [
directory for directory in os.listdir(modules_dir)
if (
os.path.isdir(os.path.join(modules_dir, directory))
and os.path.isfile(get_translation_file_path(modules_dir, directory, 'en.lproj'))
and directory != 'I18N'
)
]
return modules_list
except FileNotFoundError as e:
print(f"Directory not found: {e}", file=sys.stderr)
raise
except PermissionError as e:
print(f"Permission denied: {e}", file=sys.stderr)
raise


def get_translations(modules_dir):
"""
Retrieve the translations from all modules in the modules_dir.
Parameters:
modules_dir (str): The directory containing the modules.
Returns:
dict: A dict containing a list of dictionaries containing the 'key', 'value', and 'comment' for each
translation line. The key of the outer dict is the name of the module where the translations are going
to be saved.
"""
translations = []
try:
modules = get_modules_to_translate(modules_dir)
for module in modules:
translation_file = get_translation_file_path(modules_dir, module, lang_dir='en.lproj')
module_translation = localizable.parse_strings(filename=translation_file)

translations += [
{
'key': f"{module}.{translation_entry['key']}",
'value': translation_entry['value'],
'comment': translation_entry['comment']
} for translation_entry in module_translation
]
except Exception as e:
print(f"Error retrieving translations: {e}", file=sys.stderr)
raise

return {'I18N': translations}


def combine_translation_files(modules_dir=None):
"""
Combine translation files from different modules into a single file.
"""
try:
if not modules_dir:
modules_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
translation = get_translations(modules_dir)
write_translations_to_modules(modules_dir, 'en.lproj', translation)
except Exception as e:
print(f"Error combining translation files: {e}", file=sys.stderr)
raise


def get_languages_dirs(modules_dir):
"""
Retrieve directories containing language files for translation.
Args:
modules_dir (str): The directory containing all the modules.
Returns:
list: A list of directories containing language files for translation. Each directory represents
a specific language and ends with the '.lproj' extension.
"""
try:
lang_parent_dir = os.path.join(modules_dir, 'I18N', 'I18N')
languages_dirs = [
directory for directory in os.listdir(lang_parent_dir)
if directory.endswith('.lproj') and directory != "en.lproj"
]
return languages_dirs
except FileNotFoundError as e:
print(f"Directory not found: {e}", file=sys.stderr)
raise
except PermissionError as e:
print(f"Permission denied: {e}", file=sys.stderr)
raise


def get_translations_from_file(modules_dir, lang_dir):
"""
Get translations from the translation file in the 'I18N' directory and distribute them into the appropriate
modules' directories.
Args:
modules_dir (str): The directory containing all the modules.
lang_dir (str): The directory containing the translation file being split.
Returns:
dict: A dictionary containing translations split by module. The keys are module names,
and the values are lists of dictionaries, each containing the 'key', 'value', and 'comment'
for each translation entry within the module.
"""
translations = defaultdict(list)
try:
translations_file_path = get_translation_file_path(modules_dir, 'I18N', lang_dir)
lang_list = localizable.parse_strings(filename=translations_file_path)
for translation_entry in lang_list:
module_name, key_remainder = translation_entry['key'].split('.', maxsplit=1)
split_entry = {
'key': key_remainder,
'value': translation_entry['value'],
'comment': translation_entry['comment']
}
translations[module_name].append(split_entry)
except Exception as e:
print(f"Error extracting translations from file: {e}", file=sys.stderr)
raise
return translations


def write_translations_to_modules(modules_dir, lang_dir, modules_translations):
"""
Write translations to language files for each module.
Args:
modules_dir (str): The directory containing all the modules.
lang_dir (str): The directory of the translation file being written.
modules_translations (dict): A dictionary containing translations for each module.
Returns:
None
"""
for module, translation_list in modules_translations.items():
try:
translation_file_path = get_translation_file_path(modules_dir, module, lang_dir, create_dirs=True)
with open(translation_file_path, 'w') as f:
for translation_entry in translation_list:
write_line_and_comment(f, translation_entry)
except Exception as e:
print(f"Error writing translations to file.\n Module: {module}\n Error: {e}", file=sys.stderr)
raise


def _escape(s):
"""
Reverse the replacements performed by _unescape() in the localizable library
"""
s = s.replace('\n', r'\n').replace('\r', r'\r').replace('"', r'\"')
return s


def write_line_and_comment(f, entry):
"""
Write a translation line with an optional comment to a file.
Args:
file (file object): The file object to write to.
entry (dict): A dictionary containing the translation entry with 'key', 'value', and optional 'comment'.
Returns:
None
"""
comment = entry.get('comment') # Retrieve the comment, if present
if comment:
f.write(f"/* {comment} */\n")
f.write(f'"{entry["key"]}" = "{_escape(entry["value"])}";\n')


def split_translation_files(modules_dir=None):
"""
Split translation files into separate files for each module and language.
Args:
modules_dir (str, optional): The directory containing all the modules. If not provided,
it defaults to the parent directory of the directory containing this script.
Returns:
None
"""
try:
if not modules_dir:
modules_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
languages_dirs = get_languages_dirs(modules_dir)
for lang_dir in languages_dirs:
translations = get_translations_from_file(modules_dir, lang_dir)
write_translations_to_modules(modules_dir, lang_dir, translations)
except Exception as e:
print(f"Error splitting translation files: {e}", file=sys.stderr)
raise


def main():
try:
args = parse_arguments()
if args.split:
split_translation_files()
elif args.combine:
combine_translation_files()
except Exception as e:
print(f"An unexpected error occurred: {e}", file=sys.stderr)
raise


if __name__ == "__main__":
main()

0 comments on commit 74c4769

Please sign in to comment.