Skip to content

Commit 062a4ec

Browse files
committed
init
0 parents  commit 062a4ec

9 files changed

Lines changed: 725 additions & 0 deletions

File tree

.gitignore

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# This file is used to ignore files which are generated
2+
# ----------------------------------------------------------------------------
3+
.qtcreator
4+
*.spec
5+
6+
*~
7+
*.autosave
8+
*.a
9+
*.core
10+
*.moc
11+
*.o
12+
*.obj
13+
*.orig
14+
*.rej
15+
*.so
16+
*.so.*
17+
*_pch.h.cpp
18+
*_resource.rc
19+
*.qm
20+
.#*
21+
*.*#
22+
core
23+
!core/
24+
tags
25+
.DS_Store
26+
.directory
27+
*.debug
28+
Makefile*
29+
*.prl
30+
*.app
31+
moc_*.cpp
32+
ui_*.h
33+
qrc_*.cpp
34+
Thumbs.db
35+
*.res
36+
*.rc
37+
/.qmake.cache
38+
/.qmake.stash
39+
40+
# qtcreator generated files
41+
*.pro.user*
42+
*.qbs.user*
43+
CMakeLists.txt.user*
44+
45+
# xemacs temporary files
46+
*.flc
47+
48+
# Vim temporary files
49+
.*.swp
50+
51+
# Visual Studio generated files
52+
*.ib_pdb_index
53+
*.idb
54+
*.ilk
55+
*.pdb
56+
*.sln
57+
*.suo
58+
*.vcproj
59+
*vcproj.*.*.user
60+
*.ncb
61+
*.sdf
62+
*.opensdf
63+
*.vcxproj
64+
*vcxproj.*
65+
66+
# MinGW generated files
67+
*.Debug
68+
*.Release
69+
70+
# Python byte code
71+
*.pyc
72+
73+
# Binaries
74+
# --------
75+
*.dll
76+
*.exe
77+
78+
# Directories with generated files
79+
.moc/
80+
.obj/
81+
.pch/
82+
.rcc/
83+
.uic/
84+
/build*/

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Translate File Windows (TFW)
2+
3+
A lightweight Windows desktop tool for translating Word (`.docx`) documents between Chinese, English, French, and Spanish. The application supports optional automatic source-language detection and generates the translated file in the same directory with a language-tag suffix.
4+
5+
## Background
6+
7+
For many non-native English researchers, academic papers are often first written in their native language for clarity and efficiency. Once the manuscript reaches a mature stage, it needs to be translated into English for submission, and in some cases translated back into the local language again for internal reviews, presentations, or institutional reporting.
8+
9+
Performing paragraph-by-paragraph translation manually is time-consuming and error-prone, especially for long technical documents. This tool was developed to streamline that process while offering a basic level of content protection during translation.
10+
11+
To reduce full-document exposure when using online translation endpoints, paragraphs are translated in a randomized order. Combined with a resumable state file, users can manually switch network nodes or proxies during a run, allowing different parts of the document to be processed by different endpoints. This design provides a practical compromise between usability and privacy for completed academic work.
12+
13+
## Features
14+
15+
* Translate Word (`.docx`) documents between **zh / en / fr / es** in any direction
16+
* Optional automatic detection of the source language
17+
* Drag-and-drop a `.docx` onto the window to start translation immediately
18+
* Paragraphs are translated in a randomized order to avoid sequential content exposure
19+
* Resumable translation via a temporary state file
20+
* Supports manual switching of translation nodes or network proxies mid-run
21+
* Background translation thread with a progress bar and live status updates
22+
* Output file is automatically saved alongside the input file (example: `input.docx` -> `input-en.docx`)
23+
24+
## Download (Windows EXE)
25+
26+
To use the application, download the **prebuilt Windows executable** from the **Release page**. No Python runtime is required.
27+
28+
1. Go to the **Release** section of this repository.
29+
2. Download the latest release asset: `TranslateFileWindows.exe`.
30+
3. (Optional) Verify the checksum if provided.
31+
4. Double-click the executable to launch the application.
32+
33+
You do **not** need to clone the repository or build the project locally.
34+
35+
## Usage
36+
37+
Once launched:
38+
39+
1. Drag a Word (`.docx`) file onto the application window, or click to select a file.
40+
2. Choose the source language (or enable auto-detection) and the target language.
41+
3. Click **Start** to begin translation.
42+
4. The progress bar and status text will update as paragraphs are processed.
43+
5. After completion, the translated file is saved in the same directory as the original file, with a `-<lang>` suffix (e.g., `-en`, `-cn`, `-fr`, `-es`).
44+
45+
## Resume & Fault Tolerance
46+
47+
If the translation process stops midway, simply relaunch the program and translate the same document again.
48+
49+
* The application automatically resumes using a temporary state file stored alongside the input document.
50+
* The state file is removed after a successful run.
51+
* Paragraphs are processed in a shuffled order, so restarting the task or switching network nodes during execution does not invalidate already completed translations.

main_en2cn.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This Python file uses the following encoding: utf-8
2+
import sys
3+
4+
from PySide6.QtWidgets import QApplication
5+
6+
from widget import TranslatorWidget
7+
8+
9+
def main() -> None:
10+
"""Launch the GUI translator."""
11+
app = QApplication([])
12+
window = TranslatorWidget()
13+
window.resize(640, 200)
14+
window.show()
15+
sys.exit(app.exec())
16+
17+
18+
if __name__ == "__main__":
19+
main()

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "PySide Project"
3+
4+
[tool.pyside6-project]
5+
files = ["widget.py", "translator.py"]

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
PySide6
2+
python-docx==0.8.11
3+
googletrans==4.0.2

scripts/build.ps1

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
param(
2+
[string]$Entry = "main_en2cn.py",
3+
[string]$Name = "TranslateFileWindows"
4+
)
5+
6+
Set-StrictMode -Version Latest
7+
$ErrorActionPreference = "Stop"
8+
9+
$root = Resolve-Path (Join-Path $PSScriptRoot "..")
10+
11+
$venvActivate = Join-Path $root ".qtcreator/Python_3_12_10venv/Scripts/Activate.ps1"
12+
if (-not (Test-Path $venvActivate)) {
13+
Write-Error "Virtual env activate script not found at $venvActivate"
14+
exit 1
15+
}
16+
17+
Write-Host "Activating virtual environment ..." -ForegroundColor Cyan
18+
. $venvActivate
19+
20+
$entryPath = Join-Path $root $Entry
21+
if (-not (Test-Path $entryPath)) {
22+
Write-Error "Entry file not found: $entryPath"
23+
exit 1
24+
}
25+
26+
$icoPath = Join-Path $root "static/favicon.ico"
27+
28+
$iconArg = @("--icon", $icoPath)
29+
if (-not (Test-Path $icoPath)) {
30+
Write-Error "favicon.ico not found at $icoPath. Place your icon there before building."
31+
exit 1
32+
}
33+
34+
$buildDir = Join-Path $root "build"
35+
$distDir = Join-Path $root "dist"
36+
37+
foreach ($dir in @($buildDir, $distDir)) {
38+
if (Test-Path $dir) {
39+
Remove-Item -Recurse -Force $dir
40+
}
41+
}
42+
43+
$dataArg = @("--add-data", "$($root)\\static\\favicon.ico;static")
44+
45+
Write-Host "Building $Name from $entryPath ..." -ForegroundColor Cyan
46+
& pyinstaller --clean --noconfirm --onefile --windowed --name $Name @iconArg @dataArg $entryPath
47+
48+
if ($LASTEXITCODE -ne 0) {
49+
Write-Error "PyInstaller build failed with exit code $LASTEXITCODE"
50+
exit $LASTEXITCODE
51+
}
52+
53+
$exePath = Join-Path -Path (Join-Path $root "dist") -ChildPath "$Name.exe"
54+
if (Test-Path $exePath) {
55+
Write-Host "Build succeeded: $exePath" -ForegroundColor Green
56+
} else {
57+
Write-Warning "Build finished, but dist/$Name.exe not found."
58+
}

static/favicon.ico

231 KB
Binary file not shown.

translator.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import asyncio
2+
import json
3+
import os
4+
import random
5+
import time
6+
from typing import Generator, Tuple
7+
8+
from docx import Document
9+
from googletrans import Translator
10+
11+
# Language codes used across the app
12+
LANG_SOURCE_CODES = ["auto", "zh-cn", "en", "fr", "es"]
13+
LANG_TARGET_CODES = ["zh-cn", "en", "fr", "es"]
14+
15+
16+
def load_or_initialize_state(state_file: str, num_paragraphs: int) -> dict:
17+
if os.path.exists(state_file):
18+
with open(state_file, "r", encoding="utf-8") as f:
19+
return json.load(f)
20+
return {
21+
"translated_count": 0,
22+
"paragraphs": [],
23+
"order": [],
24+
"translated_paragraphs": [None] * num_paragraphs,
25+
}
26+
27+
28+
def save_state(state_file: str, state: dict) -> None:
29+
with open(state_file, "w", encoding="utf-8") as f:
30+
json.dump(state, f, ensure_ascii=False, indent=4)
31+
32+
33+
class DocumentTranslator:
34+
"""Translate a Word document paragraph by paragraph with resumable state."""
35+
36+
def __init__(
37+
self,
38+
input_file: str,
39+
output_file: str,
40+
state_file: str,
41+
src_lang: str = "auto",
42+
dest_lang: str = "zh-cn",
43+
) -> None:
44+
self.input_file = input_file
45+
self.output_file = output_file
46+
self.state_file = state_file
47+
self.src_lang = src_lang
48+
self.dest_lang = dest_lang
49+
50+
def translate(self) -> Generator[Tuple[int, dict], None, None]:
51+
"""
52+
Run the translation and yield (progress_percent, status_payload).
53+
54+
Progress updates can be consumed by a GUI thread without blocking the UI.
55+
"""
56+
doc = Document(self.input_file)
57+
paragraphs = [(i, para.text) for i, para in enumerate(doc.paragraphs)]
58+
59+
state = load_or_initialize_state(self.state_file, len(paragraphs))
60+
61+
if not state["paragraphs"]:
62+
state["paragraphs"] = paragraphs
63+
state["order"] = list(range(len(paragraphs)))
64+
random.shuffle(state["order"])
65+
save_state(self.state_file, state)
66+
67+
translator = Translator()
68+
loop = asyncio.new_event_loop()
69+
asyncio.set_event_loop(loop)
70+
71+
try:
72+
total = len(state["order"])
73+
for i in range(state["translated_count"], total):
74+
original_index = state["order"][i]
75+
index, text = state["paragraphs"][original_index]
76+
77+
if not text.strip():
78+
state["translated_paragraphs"][index] = text
79+
status = {
80+
"event": "skip_empty",
81+
"index": i + 1,
82+
"total": total,
83+
}
84+
else:
85+
result = translator.translate(
86+
text, src=self.src_lang, dest=self.dest_lang
87+
)
88+
# googletrans 3.4.0+ returns a coroutine; handle both sync/async
89+
if asyncio.iscoroutine(result):
90+
result = loop.run_until_complete(result)
91+
translated_text = result.text
92+
state["translated_paragraphs"][index] = translated_text
93+
status = {
94+
"event": "translated",
95+
"index": i + 1,
96+
"total": total,
97+
"src": self.src_lang,
98+
"dest": self.dest_lang,
99+
}
100+
time.sleep(random.uniform(3, 5))
101+
102+
state["translated_count"] = i + 1
103+
save_state(self.state_file, state)
104+
progress = int(((i + 1) / total) * 100)
105+
yield progress, status
106+
107+
translated_doc = Document()
108+
for para in state["translated_paragraphs"]:
109+
if para:
110+
translated_doc.add_paragraph(para)
111+
112+
translated_doc.save(self.output_file)
113+
yield 100, {
114+
"event": "completed",
115+
"output": self.output_file,
116+
"src": self.src_lang,
117+
"dest": self.dest_lang,
118+
}
119+
finally:
120+
# clean up async client
121+
try:
122+
loop.run_until_complete(translator.client.aclose())
123+
except Exception:
124+
pass
125+
loop.close()
126+
127+
# clean state only when translation completes
128+
if (
129+
os.path.exists(self.state_file)
130+
and state.get("translated_count") == len(state.get("order", []))
131+
):
132+
try:
133+
os.remove(self.state_file)
134+
except OSError:
135+
pass

0 commit comments

Comments
 (0)