forked from qt-creator/qt-creator
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
959 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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,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
130
share/qtcreator/translations/CuteLingoExpress/auto_trans.py
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,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.
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 @@ | ||
translators==5.3.1 |
66 changes: 66 additions & 0 deletions
66
share/qtcreator/translations/CuteLingoExpress/test_auto_trans.py
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,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() |
26 changes: 26 additions & 0 deletions
26
share/qtcreator/translations/CuteLingoExpress/testing/helloworld.ts
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,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
13
share/qtcreator/translations/CuteLingoExpress/testing/numerus.ts
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,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> |