Skip to content

Commit

Permalink
share: Add auto translating script.
Browse files Browse the repository at this point in the history
  • Loading branch information
kwagyeman committed Nov 23, 2024
1 parent b2e8cec commit 5d2ad4e
Show file tree
Hide file tree
Showing 8 changed files with 959 additions and 0 deletions.
674 changes: 674 additions & 0 deletions share/qtcreator/translations/CuteLingoExpress/LICENSE

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions share/qtcreator/translations/CuteLingoExpress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# CuteLingoExpress
CuteLingoExpress is a powerful tool designed to facilitate the translation of Qt ts-files for internationalization. It automates the translation process by allowing users to specify the source and target language, providing a preview of how the translated layouts will appear. This tool is particularly useful for ensuring that the app's interface is well-suited for different languages before engaging native speakers for final translations, saving time in the development cycle.

## Motivation
Internationalization plays a crucial role in developing successful applications, as not all customers are comfortable with English. Qt provides a comprehensive ecosystem for handling internationalization, including language support in C++/Qt and various tools like lupdate and Linguist. However, one missing aspect has been the ability to automatically generate translations and preview them in the context of the app's layouts. CuteLingoExpress fills this gap by automating the translation process and providing developers with a quick and convenient way to assess layout compatibility.

# Usage

## Setup
To use CuteLingoExpress, start by installing the required packages specified in
`pip install requirements.txt`
Please note that the tool currently supports the translators package version 5.3.1, as the newest version may not be compatible.

## Invocation
CuteLingoExpress is invoked by providing the path to the ts-file that needs translation and the ISO 639-1 country codes for the source and target languages. For more information about ISO 639-1 country codes, refer to the documentation available at https://pypi.org/project/translators/. The following examples demonstrate the usage:
```shell
python auto_trans.py testing/numerus.ts de cn
python auto_trans.py testing/helloworld.ts en cn
```
Upon execution, the tool will perform the translations and update the ts-file in-place. An example of the output could be as follows:
```shell
$ python auto_trans.py testing/helloworld.ts en cn
Using Germany server backend.
translateString: 0.5832037925720215s : Hello world! -> 你好世界! (en -> cn)
translateString: 1.0015525817871094s : My first dish. -> 我的第一道菜。 (en -> cn)
translateString: 1.534256935119629s : white bread with butter -> 白面包和黄油 (en -> cn)
TS file transformed successfully.
Whole execution took 3.1223082542419434s.
```

## Handling errors
* Sometimes, the chosen backend for translation, Google, may fail to start in approximately 20% of the runs. If this occurs, you can press Ctrl+C to stop the execution and retry the translation.
* Yandex and DeepL were also quite powerful, but I ran quickly into rate-limitations (watch the output).

## Checking results
* To assess the translated content, it is recommended to use the diff command from your preferred version-control system. This allows you to compare the changes made in the ts-file and verify the accuracy of the translations.
![](comparison_cn.png)

### Additional Features
* CuteLingoExpress also handles the numerus form, providing support for translation involving plurals and singulars.
* During development, a key goal was to preserve the original file structure to minimize the differences when comparing versions. This approach ensures that the changes made during translation are easily identifiable.

## Software quality
* Please execute the tests in `test_auto_trans.py` to check for regressions.
* `pylint` gives it a rating of 10/10.

# Naming?
* The name "CuteLingoExpress" combines elements from different aspects of the tool to convey its purpose and characteristics. It blends "cute" from Qt, "lingo" representing the language translation aspect, and "express" to emphasize the tool's speed and efficiency in translating Qt content. This name reflects the tool's goal of delivering delightful and rapid translations while capturing the essence of the Qt framework.
* The development of CuteLingoExpress involved applying design-thinking methodologies, leveraging GPT to enhance the overall user experience and refine the translation workflow.
130 changes: 130 additions & 0 deletions share/qtcreator/translations/CuteLingoExpress/auto_trans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
This is a Python script for translating .ts files from one language to another.
It makes use of the translators library to translate individual strings in the file,
updating the file with the new translations.
"""

import sys
import time
import xml.etree.ElementTree
import translators


def replace_first_lines(file_path):
"""
Replaces the first two lines of a file with the XML declaration and DOCTYPE.
:param file_path: The path to the file to modify.
:type file_path: str
"""
with open(file_path, 'r+', encoding='utf-8') as file:
lines = file.readlines()
lines[0] = '<?xml version="1.0" encoding="utf-8"?>\n'
lines.insert(1, '<!DOCTYPE TS>\n')

file.seek(0)
file.writelines(lines)
file.truncate()


def translate_string(source_string: str, source_language: str, target_language: str) -> str:
"""
...
:param source_string: The string to translate.
:type source_string: str
:param source_language: The ISO 639-1 code of the language to translate from.
:type source_language: str
:param target_language: The ISO 639-1 code of the language to translate to.
:type target_language: str
:return: The translated string.
:rtype: str
"""
start_time = time.time()
output = translators.google(source_string, source_language, target_language)
print(
f"translateString: {time.time() - start_time}s : {source_string} -> {output} "
f"({source_language} -> {target_language})"
)

return output


def transform_ts_file(ts_file_path, _language, target_language):
"""
Transforms a .ts file by translating all 'unfinished' messages.
The translated messages replace the original messages in the .ts file.
:param ts_file_path: The path to the .ts file to transform.
:type ts_file_path: str
:param _language: The ISO 639-1 code of the source language.
:type _language: str
:param target_language: The ISO 639-1 code of the target language.
:type target_language: str
"""
tree = xml.etree.ElementTree.parse(ts_file_path)
root = tree.getroot()

for message in root.iter('message'):
numerus = message.attrib.get('numerus') == 'yes'
if numerus:
source_text = message.find('source').text
unfinished_translation = message.find('translation')
if unfinished_translation is not None and \
unfinished_translation.attrib.get('type') == 'unfinished':
translated_text = translate_string(source_text, _language, target_language)
for num_form in unfinished_translation.findall('numerusform'):
unfinished_translation.remove(num_form) # Remove existing empty numerusforms
for _ in range(2): # assuming two forms, singular and plural
new_numerusform = xml.etree.ElementTree.SubElement(unfinished_translation,
'numerusform')
new_numerusform.text = translated_text
del unfinished_translation.attrib['type']
else:
translation = message.find('translation')
if translation is not None and translation.attrib.get('type') == 'unfinished':
source_text = message.find('source').text
translated_text = translate_string(source_text, _language, target_language)
translation.text = translated_text
del translation.attrib['type']

tree.write(ts_file_path, encoding='utf-8', xml_declaration=True)
replace_first_lines(ts_file_path) # Replace the first two lines of the output file
with open(ts_file_path, 'a', encoding='utf-8') as file: # Preserve the last empty line
file.write('\n')

print("TS file transformed successfully.")


def main():
"""
The main entry point of the script. It checks if a file path was given as
command line argument and if so, calls the function to transform the .ts file.
"""
if len(sys.argv) < 4:
print("Usage: python script.py <ts_file_path> sourceLanguage targetLanguage")
return

ts_file_path = sys.argv[1]
start_time = time.time()
transform_ts_file(ts_file_path, sys.argv[2], sys.argv[3])
print(f"Whole execution took {time.time() - start_time}s.")


if __name__ == "__main__":
main()

# test call:
# python auto_trans.py testing/helloworld.ts en cn
#
# Using Germany server backend.
# translateString: 1.3896245956420898s : Hello world! -> 你好世界! (en -> cn)
# translateString: 1.9492523670196533s : My first dish. -> 我的第一道菜。 (en -> cn)
# translateString: 2.112003803253174s : white bread with butter -> 白面包和黄油 (en -> cn)
# TS file transformed successfully.
# Whole execution took 5.453961610794067s.
# (venv) [mpetrick@marcel-precision3551 AutoTrans]$

# manual tests:
# python auto_trans.py testing/numerus.ts de cn
# python auto_trans.py testing/helloworld.ts en cn
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
translators==5.3.1
66 changes: 66 additions & 0 deletions share/qtcreator/translations/CuteLingoExpress/test_auto_trans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Unit tests for auto_trans.py.
These tests cover the functions replace_first_lines, translate_string and transform_ts_file.
"""

import unittest
from unittest.mock import patch, MagicMock
from xml.etree import ElementTree

import auto_trans


class AutoTransTest(unittest.TestCase):
"""
Test class for the auto_trans module.
"""

def setUp(self):
"""
Set up anything that is necessary for the test environment.
"""

@patch("builtins.open", new_callable=MagicMock)
def test_replace_first_lines(self, mock_open):
"""
Test that replace_first_lines replaces the first two lines of a file correctly.
"""
auto_trans.replace_first_lines("fakepath")
mock_open.assert_called_with("fakepath", 'r+', encoding='utf-8')

@patch("translators.google", return_value="你好世界")
def test_translate_string(self, mock_google):
"""
Test that translate_string calls the appropriate translation function
and returns the correct result.
"""
result = auto_trans.translate_string("Hello world", "en", "cn")
self.assertEqual(result, "你好世界")
mock_google.assert_called_once_with("Hello world", "en", "cn")

@patch("xml.etree.ElementTree.parse")
@patch("auto_trans.translate_string", return_value="你好世界")
@patch("auto_trans.replace_first_lines")
def test_transform_ts_file(self, mock_replace_first_lines, mock_translate_string, mock_parse):
"""
Test that transform_ts_file updates a .ts file correctly.
"""
fake_tree = ElementTree.ElementTree(ElementTree.Element("TS"))
fake_msg = ElementTree.Element("message")
fake_source = ElementTree.Element("source")
fake_source.text = "Hello world"
fake_translation = ElementTree.Element("translation", attrib={"type": "unfinished"})
fake_msg.extend([fake_source, fake_translation])
fake_tree.getroot().append(fake_msg)
mock_parse.return_value = fake_tree

auto_trans.transform_ts_file("fakepath", "en", "cn")

mock_translate_string.assert_called_once_with("Hello world", "en", "cn")
mock_replace_first_lines.assert_called_once_with("fakepath")
self.assertIsNone(fake_translation.attrib.get('type'))
self.assertEqual(fake_translation.text, "你好世界")


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
<name>QPushButton</name>
<message>
<source>Hello world!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>My first dish.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>recipe name</name>
<message>
<source>white bread with butter</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Green tea</source>
<translation>Grüner Tee</translation>
</message>
</context>
</TS>
13 changes: 13 additions & 0 deletions share/qtcreator/translations/CuteLingoExpress/testing/numerus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
<name>QPushButton</name>
<message numerus="yes">
<source>+ %n min</source>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
</TS>

0 comments on commit 5d2ad4e

Please sign in to comment.