-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from RedDeadDepresso/test
English translation for the GUI
- Loading branch information
Showing
85 changed files
with
5,846 additions
and
292 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
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,175 @@ | ||
import os | ||
import subprocess | ||
import translators as ts | ||
|
||
from bs4 import BeautifulSoup | ||
from lxml import etree | ||
from gui.util.language import Language | ||
|
||
|
||
class Handler: | ||
def set_next(self, request): | ||
request.handlers.pop(0) | ||
if request.handlers: | ||
request.handlers[0].handle(request) | ||
|
||
def handle(self, request): | ||
pass | ||
|
||
|
||
class Request: | ||
def __init__(self, handlers: list[Handler], | ||
qt_language: Language, | ||
translator: str = 'bing', | ||
from_lang: str = 'auto', | ||
to_lang: str = 'en'): | ||
""" | ||
Parameters | ||
---------- | ||
handlers: list[Handler] | ||
a list of handlers that represent the files to translate. | ||
qt_language: Language | ||
the memeber of the enum Language to translate | ||
translator: str | ||
see https://github.com/uliontse/translators | ||
from_lang: str | ||
see https://github.com/uliontse/translators | ||
to_lang: str | ||
see https://github.com/uliontse/translators | ||
""" | ||
self.qt_language = qt_language | ||
self.strLang = qt_language.value.name() | ||
self.handlers = handlers | ||
self.translator = translator | ||
self.from_lang = from_lang | ||
self.to_lang = to_lang | ||
|
||
def translate_text(self, text): | ||
text = ts.translate_text(text, self.translator, self.from_lang, self.to_lang) | ||
print(text) | ||
return text | ||
|
||
def translate_html(self, html_text): | ||
return ts.translate_html(html_text, self.translator, self.from_lang, self.to_lang) | ||
|
||
def process(self): | ||
self.handlers[0].handle(self) | ||
|
||
|
||
class Pylupdate5Handler(Handler): | ||
def handle(self, request): | ||
result = subprocess.run(['pylupdate5', 'i18n.pro'], capture_output=True, text=True) | ||
print(result.stdout) | ||
self.set_next(request) | ||
|
||
|
||
class XmlHandler(Handler): | ||
"""Translate ts files""" | ||
def handle(self, request): | ||
# Load the XML from a file | ||
input_file = os.path.join('../gui/i18n/', f'{request.strLang}.ts') | ||
output_file = os.path.join('../gui/i18n/', f'{request.strLang}.ts') | ||
|
||
tree = etree.parse(input_file) | ||
root = tree.getroot() | ||
|
||
# Find all 'source' tags and translate their text | ||
for source in root.iter('source'): | ||
|
||
# Find the 'translation' tag within the parent 'message' tag | ||
translation = source.getparent().find('translation') | ||
|
||
# Check the 'type' attribute of the 'translation' tag | ||
if 'type' in translation.attrib: | ||
if translation.attrib['type'] == 'obsolete': | ||
# Delete the parent 'message' tag if 'type' is 'obsolete' | ||
source.getparent().getparent().remove(source.getparent()) | ||
elif translation.attrib['type'] == 'unfinished' and not translation.text: | ||
# Update the 'translation' tag if 'type' is not 'obsolete' | ||
translation.text = request.translate_text(source.text) | ||
else: | ||
# Don't update the 'translation' tag if 'type' attribute doesn't exist | ||
continue | ||
|
||
# Write the updated XML back to the file | ||
tree.write(output_file, encoding='utf8') | ||
self.set_next(request) | ||
|
||
|
||
class HtmlHandler(Handler): | ||
"""Generate descriptions""" | ||
def translate_mission_types(self, request, input_dir, output_dir): | ||
input_path = os.path.join(input_dir, '各区域所需队伍属性.html') | ||
output_path = os.path.join(output_dir, request.translate_text('各区域所需队伍属性') + '.html') | ||
|
||
translations = { | ||
'爆发': 'Explosive', | ||
'贯穿': 'Piercing', | ||
'神秘': 'Mystic', | ||
'振动': 'Sonic' | ||
} | ||
|
||
with open(input_path, 'r') as f: | ||
text = f.read() | ||
|
||
for original, translated in translations.items(): | ||
text = text.replace(original, translated) | ||
|
||
with open(output_path, 'w') as f: | ||
f.write(text) | ||
|
||
def handle(self, request): | ||
input_dir = '../src/descriptions/' | ||
output_dir = f'src/descriptions/{request.strLang}' | ||
# Ensure the output directory exists | ||
if not os.path.exists(output_dir): | ||
os.makedirs(output_dir) | ||
|
||
# Iterate over all files in the input directory | ||
for filename in os.listdir(input_dir): | ||
if request.strLang == 'en_us' and filename == '各区域所需队伍属性.html': | ||
self.translate_mission_types(request, input_dir, output_dir) | ||
continue | ||
|
||
if filename.endswith('.html'): | ||
# Parse the HTML file with BeautifulSoup | ||
with open(os.path.join(input_dir, filename), 'r') as f: | ||
html = f.read() | ||
translated_html = request.translate_html(html) | ||
soup = BeautifulSoup(translated_html, 'lxml') | ||
prettyHTML = soup.prettify() | ||
|
||
# Write the translated HTML to the output directory | ||
name, extension = os.path.splitext(filename) | ||
output_name = f'{request.translate_text(name)}.html' | ||
with open(os.path.join(output_dir, output_name), 'w') as f: | ||
f.write(prettyHTML) | ||
|
||
|
||
class LreleaseHandler(Handler): | ||
def handle(self, request): | ||
directory = os.path.join(os.getcwd(), '../gui', 'i18n') | ||
result = subprocess.run(['lrelease', f'{request.strLang}.ts'], cwd=directory, capture_output=True, text=True) | ||
print(result.stdout) | ||
self.set_next(request) | ||
|
||
|
||
if __name__ == "__main__": | ||
pylupdate = Pylupdate5Handler() | ||
gui = XmlHandler() | ||
descriptions = HtmlHandler() | ||
lrelease = LreleaseHandler() | ||
|
||
# request_en = Request([pylupdate, gui, descriptions, lrelease], Language.ENGLISH, 'bing', 'zh-Hans', 'en') | ||
# request_en.process() | ||
|
||
request_en = Request([pylupdate, gui, lrelease], Language.ENGLISH, 'bing', 'zh-Hans', 'en') | ||
request_en.process() | ||
|
||
request_jp = Request([gui], Language.JAPANESE, 'bing', 'zh-Hans', 'ja') | ||
request_jp.process() | ||
|
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,184 @@ | ||
# i18n Guide | ||
|
||
## Requirements | ||
|
||
- [Qt Linguist](https://github.com/thurask/Qt-Linguist/releases) | ||
- [OPTIONAL] | ||
``` | ||
pip install -r requirements-i18n.txt | ||
``` | ||
|
||
## Translation | ||
|
||
### Adding a New Language | ||
To include a new language, add a new member to the Language enum representing the language with its corresponding QLocale object. Then, update the combobox method to return a list of language names based on the enum members order. For example, to add Japanese: | ||
``` | ||
class Language(Enum): | ||
""" Language enumeration """ | ||
CHINESE_SIMPLIFIED = QLocale(QLocale.Chinese, QLocale.China) | ||
ENGLISH = QLocale(QLocale.English, QLocale.UnitedStates) | ||
JAPANESE = QLocale(QLocale.Japanese, QLocale.Japan) | ||
def combobox(): | ||
return ['简体中文', 'English', '日本語'] | ||
``` | ||
|
||
Run the Python file; it will print the language acronym: | ||
|
||
``` | ||
ja_JP | ||
``` | ||
|
||
Open `i18n.pro` and modify the `TRANSLATION` variable by appending `gui/i18n/` + acronym: | ||
|
||
```pro | ||
TRANSLATIONS += gui/i18n/en_US.ts \ | ||
gui/i18n/ja_JP.ts \ | ||
``` | ||
|
||
Execute the following command to generate `.ts` files: | ||
|
||
``` | ||
pylupdate5 i18n.pro | ||
``` | ||
|
||
OPTIONAL: `auto_translate.py` | ||
|
||
Utilize `argostranslate` to accelerate translations after installing the necessary packages from `requirements-i18n.txt`. Note that while translations may not be perfect, they can speed up the process. | ||
|
||
Simply create a new `Request` instance at the end of `auto_translate.py` and invoke its `process` method. | ||
|
||
The `Request` constructor is as follows: | ||
|
||
```python | ||
class Request: | ||
from_code = "zh" | ||
|
||
def __init__(self, handlers: list[Handler], language: Language, argos_model: str): | ||
""" | ||
Parameters | ||
---------- | ||
handlers: list[Handler] | ||
a list of handlers that represent the files to translate. | ||
language: Language | ||
the memeber of the enum Language to translate | ||
argos_model: str | ||
The argos model to load for translation | ||
""" | ||
``` | ||
|
||
Then: | ||
|
||
```python | ||
model = ModelHandler() | ||
ts = XmlHandler() | ||
descriptions = HtmlHandler() | ||
|
||
request_jp = Request([model, ts, descriptions], Language.JAPANESE, 'ja') | ||
request_jp.process() | ||
``` | ||
|
||
This means that `.ts` and description files will be generated. You can adjust the list of handlers as needed, but `model` must always be the first element. | ||
|
||
Also, in case no model exists for your language, you could create a new subclass of Request, override its translate method to use another Python library, and omit ModelHandler from the list of handlers. | ||
|
||
Open `Qt Linguist`, load the `.ts` file, and manually translate. This step will require some time. | ||
|
||
Afterward, navigate to `gui/i18n` and execute the following command: | ||
|
||
``` | ||
lrelease ja_JP.ts | ||
``` | ||
|
||
This will produce the `.qm` files. | ||
|
||
## `baasTranslator` | ||
|
||
### Problems | ||
In normal scenarios, the `tr` method of `QObject` is used for translation, inherited by most PyQt5 classes. However, this approach isn't always possible for `baas` due to: | ||
|
||
- Widget text being generated from JSON files | ||
- Need to retain user input (e.g., combobox selections) in Chinese within config files | ||
|
||
### Solution | ||
To address this: | ||
|
||
- `ConfigTranslation`: a `QObject` subclass with a dictionary attribute mapping text enclosed in `self.tr` to itself: | ||
|
||
```python | ||
... | ||
self.entries = { | ||
# display | ||
self.tr("每日特别委托"): "每日特别委托", | ||
... | ||
``` | ||
|
||
- Utilize a specific instance of `QTranslator` named `baasTranslator` from `gui/util/translator.py` as `bt`, employing its methods: | ||
|
||
- `tr(context, sourceText)` | ||
- `undo(text)` | ||
|
||
For instance, `bt.tr('ConfigTranslation', '国际服')` will produce its translation, 'Global', and `bt.undo('Global')` will revert it to '国际服'. This functionality is already implemented in `get` and `set` method of `ConfigSet`. | ||
|
||
In essence, `tr` accesses the `.qm` file to retrieve translations based on the `ConfigTranslation` context, while `undo` retrieves the value from the mapped dictionary where the translation was stored. | ||
|
||
Note that contexts other than `'ConfigTranslation'` are accessible. For example: | ||
|
||
```python | ||
class Layout(TemplateLayout): | ||
def __init__(self, parent=None, config=None): | ||
configItems = [ | ||
{ | ||
'label': '一键反和谐', | ||
'type': 'button', | ||
'selection': self.fhx, | ||
'key': None | ||
}, | ||
... | ||
super().__init__(parent=parent, configItems=configItems, config=config) | ||
``` | ||
|
||
Suppose you want to translate the `label` and `selection` but can't access the `tr` method directly since you must first invoke the parent constructor and wish to avoid extensive modifications. Here's a solution: | ||
|
||
```python | ||
class Layout(TemplateLayout): | ||
def __init__(self, parent=None, config=None): | ||
OtherConfig = QObject() | ||
configItems = [ | ||
{ | ||
'label': OtherConfig.tr('一键反和谐'), | ||
'type': 'button', | ||
'selection': self.fhx, | ||
'key': None | ||
}, | ||
... | ||
super().__init__(parent=parent, configItems=configItems, config=config, context="OtherConfig") | ||
``` | ||
|
||
1. Add a new `context` parameter to the `TemplateLayout` constructor. | ||
2. Create an instance of `QObject` named `OtherConfig`. This establishes a new context when generating the `.ts` files, allowing you to pass `'OtherConfig'` to the `TemplateLayout` constructor. | ||
|
||
Now, accessing translations in `TemplateLayout` becomes straightforward: | ||
|
||
```python | ||
class TemplateLayout(QWidget): | ||
patch_signal = pyqtSignal(str) | ||
|
||
def __init__(self, configItems: Union[list[ConfigItem], list[dict]], parent=None, config=None, context=None): | ||
... | ||
labelComponent = QLabel(bt.tr(context, cfg.label), self) | ||
``` | ||
|
||
## Adding new GUI .py files | ||
Please ensure to include the file path of the new GUI .py files into the `i18n.pro` file's `SOURCES` variable, taking care to use the correct slashes. For instance, if you're adding `table.py` located in `gui/components`, it should be appended like this: | ||
|
||
``` | ||
SOURCES += \ | ||
gui/components/table.py \ | ||
``` | ||
|
||
## Adding new Config value | ||
When adding a new config value that appears on the GUI and needs translation, update the `ConfigTranslation` dictionary by adding a new key-value pair. For comboboxes, make sure to include all options. |
Oops, something went wrong.