Skip to content
This repository has been archived by the owner on May 31, 2022. It is now read-only.

🌐 Interactive Translation Manager #150

Merged
merged 13 commits into from Oct 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added images/interactive_translation.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions tools/README.md
@@ -0,0 +1,19 @@
# Translation Manager

If you are familiar with the command line, you might consider using the interactive translation manager for a much faster translation process! ⚡️
![interactive_translation](../images/interactive_translation.gif)

## How to use it

Run the following command inside this folder to spawn an interactive translation shell:

```bash
python translation_manager.py translate_interactive <TARGET_LANGUAGE>
```

You will be asked to submit the missing translation keys.
Replace `<TARGET_LANGUAGE>` with the language you want to translate to.
The provided language must be a [short letter code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (ISO 639-1).

> Note: If you want to enforce recreating all keys, pass the flag `-recreate` to the end of the above command.
This might be useful if you want to proof-check keys, as it also shows you the English source sentence alongside the current translation. Translations that you consider good can be simply inherited when you leave the prompt empty.
162 changes: 156 additions & 6 deletions tools/translation_manager.py
@@ -1,20 +1,57 @@
#!/usr/bin/env python3

from functools import reduce
import operator
import json
import copy
import glob

import os
import sys
import fire

# For better keyboard support, try importing 'readline' package (not required)
try:
import readline
except ImportError:
pass


LOCALE_DIR = "../locales"
INDENT = 4


def open_locale_file(fname):
"""Opens a locale file"""
with open(fname) as json_file:
return json.load(json_file)
class InvalidLanguageError(Exception):
"""Exception raised for invalid language inputs.

Attributes
----------
`language` : input language which caused the error
"""

def __init__(self, language=""):
self.language = language
super().__init__(self.language)

def __str__(self):
if self.language:
return f"Invalid language {self.language}, or the {self.language}.json was not found!\nPlease check if it is an ISO 639-1 language flag! "
else:
return f"Invalid language, or the JSON was not found!\nPlease check if it is an ISO 639-1 language flag!"


def open_locale_file(fname) -> dict:
"""Opens a locale file.

Raises
------
`InvalidLanguageError`
If the input language flag is invalid.
"""
try:
with open(fname) as json_file:
return json.load(json_file)
except FileNotFoundError:
raise InvalidLanguageError()


def write_json(data):
Expand Down Expand Up @@ -87,6 +124,46 @@ def get_locale_files(path=LOCALE_DIR):
return glob.glob(f'{path}/*.json')


def recurse_dict(d: dict, keys=()):
"""Generator.

Returns
-------
Iterable of the nested dictionary as (compound_keys, value):
- compound_keys: cookie-trail
- values: nested value after following the compound_keys
"""
if type(d) == dict:
for key in d:
for value in recurse_dict(d[key], keys + (key, )):
yield value
else:
yield (keys, d)

def nested_exists(d: dict, *keys):
"""Checks if nested key exists."""
try:
nested_get(d, *keys)
except KeyError:
return False
return True

def nested_get(d: dict, *keys):
"""Returns value of nested dict for given compound_keys."""
return reduce(operator.getitem, keys, d)

def nested_set(d: dict, value, *keys):
"""Sets value in nested dictionary for given keys path."""
#nested_get(d, *keys[:-1])[keys[-1]] = value
if len(keys) == 1:
d[keys[0]] = value
else:
try:
nested_set(d[keys[0]], value, *keys[1:])
except KeyError:
d[keys[0]] = {}
nested_set(d[keys[0]], value, *keys[1:])

class TranslationManager(object):
"""Command line tool for managing i18n files"""
def alphabetize(self):
Expand Down Expand Up @@ -136,6 +213,79 @@ def trim_dead_keys(self, locale="en"):
trim_dead_keys(primary_dict, dest_dict)
export(dest_dict, fname)

def translate_interactive(self, dest: str, recreate=False):
"""Spawns interactive translating session.

User is asked to submit missing key-translations for a given language.

Parameters
----------
`dest` : str
The destination language flag [ISO 639-1].
`recreate` : bool
If true, all keys will be re-translated (not only missing keys!).
"""
# Load locale
source_fname = f"{LOCALE_DIR}/en.json"
dest_fname = f"{LOCALE_DIR}/{dest}.json"
source_dict = open_locale_file(source_fname)
dest_dict = open_locale_file(dest_fname)
print("Selected translation language:", dest)
if recreate:
print("WARNING: Entered mode for re-translating all keys!")

# Check which keys do not exist in target dict
queue = []
for compound_key, _ in recurse_dict(source_dict):
if not nested_exists(dest_dict, *compound_key) or recreate:
queue.append(compound_key)

# Return if nothing to translate
n_keys = len(queue)
if n_keys == 0:
print(">>>> All keys are set, nothing to translate! 🥳")
return

# Start interactive translation session
print("Enter a translation for the given key.")
print("Leave it empty if the current translation is good enough, or you are unsure.")
cols, _ = os.get_terminal_size()
print("-"*cols)
for i, compound_key in enumerate(queue):
# Prompt user to input new translation
counter = f"## {i+1} out of {n_keys} keys to translate ##"
key_trail = "## Key: " + '->'.join(compound_key) + " ##"
string_source = f"en: {nested_get(source_dict, *compound_key)}"
string_dest = f"{dest}: {nested_get(dest_dict, *compound_key) if nested_exists(dest_dict, *compound_key) else '???'}"
prompt = ">> "
print(counter)
print(key_trail)
print(string_source)
print(string_dest)
string_translated = input(prompt)

# Clear terminal
cols, _ = os.get_terminal_size()
for i in range(int(len(counter)/cols)
+ int(len(key_trail)/cols)
+ int(len(string_source)/cols)
+ int(len(string_dest)/cols)
+ int((len(prompt)+len(string_translated))/cols)
+ 5):
sys.stdout.write("\033[F") # back to previous line
sys.stdout.write("\033[K") # clear line

# Save user input to dict
if string_translated:
nested_set(dest_dict, string_translated, *compound_key)

# export modified dict and save
export(dest_dict, dest_fname)
print("Done! 🎉")


if __name__ == '__main__':
fire.Fire(TranslationManager)
try:
fire.Fire(TranslationManager)
except KeyboardInterrupt:
print("\nAbort.")