Skip to content

Commit

Permalink
Merge pull request #113 from RedDeadDepresso/test
Browse files Browse the repository at this point in the history
English translation for the GUI
  • Loading branch information
pur1fying committed Jul 17, 2024
2 parents 851bc2e + 8aa3030 commit 4dc7378
Show file tree
Hide file tree
Showing 85 changed files with 5,846 additions and 292 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
!ui.png
!window.py
!window.spec
!i18n.pro
!requirements-i18n.txt
src/explore_task_data/__pycache__/normal_task_11.cpython-39.pyc
*.pyc
*.xml
Expand Down
4 changes: 3 additions & 1 deletion core/picture.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time
from core import color, image

from module.main_story import change_acc_auto

def co_detect(self, rgb_ends=None, rgb_possibles=None, img_ends=None, img_possibles=None, skip_first_screenshot=False,
tentitive_click=False, tentitivex=1238, tentitivey=45, max_fail_cnt=10, rgb_pop_ups=None,
Expand Down Expand Up @@ -118,6 +118,8 @@ def deal_with_pop_ups(self, rgb_pop_ups, img_pop_ups):
if color.judgeRGBFeature(self, position):
self.logger.info("find : " + position)
if position == "fighting_feature":
self.logger.info("Enter fight, wait fight auto end")
change_acc_auto(self)
img_possibles = {
"normal_task_mission-operating-task-info-notice": (995, 101),
"normal_task_end-turn": (890, 162),
Expand Down
175 changes: 175 additions & 0 deletions develop_tools/auto_translate.py
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()

2 changes: 1 addition & 1 deletion develop_tools/explore_task_data_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# this script is used to generate explore task data which can be used
# in function common_gird_method in module.explore_normal_task, the function is under this line
# in function common_gird_method which is under this line in module.explore_normal_task
from module.explore_normal_task import common_gird_method


Expand Down
184 changes: 184 additions & 0 deletions docs/i18n.md
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.
Loading

0 comments on commit 4dc7378

Please sign in to comment.