Initial commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dinlo
2026-05-31 18:45:49 +08:00
commit 5dc00d6e70
21 changed files with 103910 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,254 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), http.server (delayed, optional), psutil (optional), netrc (delayed, conditional), getpass (delayed)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional), pty (delayed, optional)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by tty (top-level), getpass (optional), click._termui_impl (conditional)
missing module named urllib.urlopen - imported by urllib (delayed, optional), lxml.html (delayed, optional)
missing module named urllib.urlencode - imported by urllib (delayed, optional), lxml.html (delayed, optional)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional), posixpath (optional)
missing module named resource - imported by posix (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named pyimod02_importers - imported by C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named annotationlib - imported by typing_extensions (conditional), attr._compat (conditional)
missing module named _dummy_thread - imported by numpy.core.arrayprint (optional)
missing module named startup - imported by pyreadline3.keysyms.common (conditional), pyreadline3.keysyms.keysyms (conditional)
missing module named sets - imported by pyreadline3.keysyms.common (optional)
missing module named System - imported by pyreadline3.clipboard.ironpython_clipboard (top-level), pyreadline3.keysyms.ironpython_keysyms (top-level), pyreadline3.console.ironpython_console (top-level), pyreadline3.rlmain (conditional)
missing module named console - imported by pyreadline3.console.ansi (conditional)
missing module named clr - imported by pyreadline3.clipboard.ironpython_clipboard (top-level), pyreadline3.console.ironpython_console (top-level)
missing module named IronPythonConsole - imported by pyreadline3.console.ironpython_console (top-level)
missing module named numpy.core.result_type - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.float_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.number - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.object_ - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.max - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.all - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.errstate - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.bool_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.inf - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isnan - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.array2string - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.imag - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.real - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.iscomplexobj - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.signbit - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isscalar - imported by numpy.core (delayed), numpy.testing._private.utils (delayed), numpy.lib.polynomial (top-level)
missing module named numpy.core.array - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isnat - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.ndarray - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.array_repr - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.arange - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.empty - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.float32 - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.intp - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _winreg - imported by platform (delayed, optional), pygments.formatters.img (optional)
missing module named numpy.core.linspace - imported by numpy.core (top-level), numpy.lib.index_tricks (top-level)
missing module named numpy.core.iinfo - imported by numpy.core (top-level), numpy.lib.twodim_base (top-level)
missing module named numpy.core.transpose - imported by numpy.core (top-level), numpy.lib.function_base (top-level)
missing module named numpy.uint - imported by numpy (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.core.asarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.utils (top-level), numpy.fft._pocketfft (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.integer - imported by numpy.core (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.sqrt - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.conjugate - imported by numpy.core (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.swapaxes - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.zeros - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.reciprocal - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.argsort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sign - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.count_nonzero - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.divide - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.matmul - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.asanyarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.atleast_2d - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.prod - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amax - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amin - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.moveaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.geterrobj - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.finfo - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isfinite - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sum - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.multiply - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.add - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.dot - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.Inf - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.newaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.complexfloating - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.inexact - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.cdouble - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.csingle - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.double - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.single - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.intc - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.empty_like - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named pyodide_js - imported by threadpoolctl (delayed, optional)
missing module named numpy.core.ufunc - imported by numpy.core (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.ones - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.hstack - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_1d - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_3d - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named numpy.core.vstack - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named pickle5 - imported by numpy.compat.py3k (optional)
missing module named numpy.eye - imported by numpy (delayed), numpy.core.numeric (delayed)
missing module named numpy.recarray - imported by numpy (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.expand_dims - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.array - imported by numpy (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.iscomplexobj - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amin - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amax - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.isinf - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isnan - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isfinite - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.float64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.float32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level)
missing module named numpy.uint32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level)
missing module named numpy.uint16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.bytes_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.str_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.void - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.object_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.datetime64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.timedelta64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.number - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.complexfloating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.floating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.integer - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ctypeslib (top-level)
missing module named numpy.unsignedinteger - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.bool_ - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.generic - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.dtype - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.array_api._typing (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ndarray - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ufunc - imported by numpy (top-level), numpy._typing (top-level), numpy.testing.overrides (top-level)
missing module named numpy.histogramdd - imported by numpy (delayed), numpy.lib.twodim_base (delayed)
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
missing module named numpy._typing._ufunc - imported by numpy._typing (conditional)
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
missing module named defusedxml - imported by PIL.Image (optional)
missing module named PyObjCTools - imported by darkdetect._mac_detect (optional)
missing module named Foundation - imported by darkdetect._mac_detect (optional)
missing module named collections.Callable - imported by collections (optional), bs4.element (optional), bs4.builder._lxml (optional)
missing module named six.moves.xrange - imported by six.moves (top-level), langdetect.detector (top-level), langdetect.utils.lang_profile (top-level)
runtime module named six.moves - imported by langdetect.detector (top-level), langdetect.utils.lang_profile (top-level)
missing module named six.moves.zip - imported by six.moves (top-level), langdetect.detector (top-level)
missing module named StringIO - imported by six (conditional)
missing module named simplejson - imported by requests.compat (conditional, optional), langdetect.detector_factory (optional)
missing module named pypdf - imported by deep_translator.base (delayed)
missing module named docx2txt - imported by deep_translator.base (delayed)
missing module named dummy_threading - imported by requests.cookies (optional)
missing module named backports - imported by urllib3.util.request (conditional, optional), urllib3.response (conditional, optional)
missing module named compression - imported by urllib3.util.request (conditional, optional), urllib3.response (conditional, optional)
missing module named 'h2.events' - imported by urllib3.http2.connection (top-level), httpcore._sync.http2 (top-level), httpcore._async.http2 (top-level)
missing module named 'h2.connection' - imported by urllib3.http2.connection (top-level), httpcore._sync.http2 (top-level), httpcore._async.http2 (top-level)
missing module named h2 - imported by urllib3.http2.connection (top-level), httpx._client (delayed, conditional, optional)
missing module named brotlicffi - imported by urllib3.util.request (optional), urllib3.response (optional), httpx._decoders (optional)
missing module named socks - imported by urllib3.contrib.socks (optional)
missing module named cryptography.x509.UnsupportedExtension - imported by cryptography.x509 (optional), urllib3.contrib.pyopenssl (optional)
missing module named 'OpenSSL.crypto' - imported by urllib3.contrib.pyopenssl (delayed, conditional)
missing module named OpenSSL - imported by urllib3.contrib.pyopenssl (top-level)
missing module named chardet - imported by requests (optional), pygments.lexer (delayed, conditional, optional), bs4.dammit (optional)
missing module named 'pyodide.ffi' - imported by urllib3.contrib.emscripten.fetch (delayed, optional)
missing module named pyodide - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named js - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named cchardet - imported by bs4.dammit (optional)
missing module named bs4.builder.HTMLParserTreeBuilder - imported by bs4.builder (top-level), bs4 (top-level)
missing module named htmlentitydefs - imported by lxml.html.soupparser (optional)
missing module named BeautifulSoup - imported by lxml.html.soupparser (optional)
missing module named urlparse - imported by lxml.ElementInclude (optional), lxml.html.html5parser (optional)
missing module named urllib2 - imported by lxml.ElementInclude (optional), lxml.html.html5parser (optional)
missing module named 'html5lib.treebuilders' - imported by bs4.builder._html5lib (optional), lxml.html._html5builder (top-level), lxml.html.html5parser (top-level)
missing module named html5lib - imported by bs4.builder._html5lib (top-level), lxml.html.html5parser (top-level)
missing module named 'cython.cimports' - imported by lxml.html.diff (optional)
missing module named cython - imported by pydantic.v1.version (optional), lxml.html.diff (optional), lxml.html._difflib (optional)
missing module named lxml_html_clean - imported by lxml.html.clean (optional)
missing module named cssselect - imported by lxml.cssselect (optional)
missing module named 'html5lib.constants' - imported by bs4.builder._html5lib (top-level)
missing module named email_validator - imported by pydantic.networks (delayed, conditional, optional), pydantic.v1.networks (delayed, conditional, optional), pydantic.v1._hypothesis_plugin (optional)
missing module named toml - imported by pydantic.v1.mypy (delayed, conditional, optional)
missing module named tomli - imported by pydantic.mypy (delayed, conditional, optional), pydantic.v1.mypy (delayed, conditional, optional)
missing module named 'mypy.version' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.util' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.typevars' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.types' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.server' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.semanal' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.plugins' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.plugin' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.options' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.nodes' - imported by pydantic.mypy (top-level), pydantic.v1.mypy (top-level)
missing module named 'mypy.errorcodes' - imported by pydantic.v1.mypy (top-level)
missing module named 'IPython.core' - imported by rich.pretty (delayed, optional), dotenv.ipython (top-level)
missing module named hypothesis - imported by pydantic.v1._hypothesis_plugin (top-level)
missing module named 'mypy.typeops' - imported by pydantic.mypy (top-level)
missing module named 'mypy.state' - imported by pydantic.mypy (top-level)
missing module named 'mypy.expandtype' - imported by pydantic.mypy (top-level)
missing module named mypy - imported by pydantic.mypy (top-level)
missing module named _typeshed - imported by pydantic_core._pydantic_core (top-level), pydantic._internal._dataclasses (conditional), anyio.abc._eventloop (conditional), anyio._core._sockets (conditional), anyio._core._fileio (conditional), anyio._core._tempfile (conditional), httpx._transports.wsgi (conditional), anyio._backends._asyncio (conditional), anyio._core._asyncio_selector_thread (conditional), anyio._backends._trio (conditional)
missing module named eval_type_backport - imported by pydantic._internal._typing_extra (delayed, optional)
missing module named pygments.lexers.PrologLexer - imported by pygments.lexers (top-level), pygments.lexers.cplint (top-level)
missing module named ctags - imported by pygments.formatters.html (optional)
missing module named IPython - imported by rich.jupyter (delayed, optional)
missing module named ipywidgets - imported by rich.live (delayed, conditional, optional)
missing module named 'IPython.display' - imported by rich.live (delayed, conditional, optional)
missing module named linkify_it - imported by markdown_it.main (optional)
missing module named pydantic.PydanticUserError - imported by pydantic (top-level), pydantic.root_model (top-level)
missing module named pydantic.PydanticSchemaGenerationError - imported by pydantic (delayed), pydantic.functional_validators (delayed, conditional)
missing module named pydantic.BaseModel - imported by pydantic (conditional), pydantic._internal._typing_extra (conditional), pydantic._internal._import_utils (delayed, conditional), pydantic.deprecated.copy_internals (delayed, conditional), openai.resources.beta.realtime.realtime (top-level)
missing module named curio - imported by sniffio._impl (delayed, conditional)
missing module named 'trio.testing' - imported by anyio._backends._trio (delayed)
missing module named exceptiongroup - imported by anyio._core._exceptions (conditional), anyio._core._sockets (conditional), anyio._backends._asyncio (conditional), anyio._backends._trio (conditional)
missing module named 'trio.to_thread' - imported by anyio._backends._trio (top-level)
missing module named 'trio.socket' - imported by anyio._backends._trio (top-level)
missing module named outcome - imported by anyio._backends._trio (top-level)
missing module named 'trio.lowlevel' - imported by anyio._backends._trio (top-level)
missing module named 'trio.from_thread' - imported by anyio._backends._trio (top-level)
missing module named _pytest - imported by anyio._backends._asyncio (delayed)
missing module named winloop - imported by anyio._backends._asyncio (delayed, conditional)
missing module named uvloop - imported by anyio._backends._asyncio (delayed, conditional)
missing module named trio - imported by httpx._transports.asgi (delayed, conditional), httpcore._synchronization (optional), httpcore._backends.trio (top-level), openai.resources.vector_stores.file_batches (delayed, conditional)
missing module named pandas - imported by openai._extras.pandas_proxy (delayed, conditional, optional)
missing module named websockets.speedups - imported by websockets.frames (optional), websockets.legacy.framing (optional)
missing module named websockets.Subprotocol - imported by websockets (conditional), openai.types.websocket_connection_options (conditional)
missing module named 'websockets.asyncio' - imported by openai.resources.beta.realtime.realtime (delayed, conditional, optional)
missing module named jiter.from_json - imported by jiter (top-level), openai.lib.streaming.chat._completions (top-level)
missing module named socksio - imported by httpcore._sync.socks_proxy (top-level), httpcore._async.socks_proxy (top-level), httpx._transports.default (delayed, conditional, optional)
missing module named 'h2.settings' - imported by httpcore._sync.http2 (top-level), httpcore._async.http2 (top-level)
missing module named 'h2.exceptions' - imported by httpcore._sync.http2 (top-level), httpcore._async.http2 (top-level)
missing module named 'h2.config' - imported by httpcore._sync.http2 (top-level), httpcore._async.http2 (top-level)
missing module named '_typeshed.wsgi' - imported by httpx._transports.wsgi (conditional)
missing module named zstandard - imported by httpx._decoders (optional)
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+261
View File
@@ -0,0 +1,261 @@
import os
import time
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
import customtkinter as ctk
from deep_translator import GoogleTranslator
from langdetect import detect, LangDetectException
# ==========================================
# НАСТРОЙКИ ПРИЛОЖЕНИЯ И ПЕРЕВОДЧИКА
# ==========================================
# Настройки внешнего вида CustomTkinter
ctk.set_appearance_mode("System") # Подстраивается под темную/светлую тему ОС
ctk.set_default_color_theme("blue") # Цветовой акцент
# Получаем языки
translator_api = GoogleTranslator()
LANGUAGES_DICT = translator_api.get_supported_languages(as_dict=True)
LANGUAGES_LIST =[lang.capitalize() for lang in LANGUAGES_DICT.keys()]
# Лимиты Google
API_TIMEOUT = 1.0
MAX_TEXT_LEN = 4800
# ==========================================
# БЭКЭНД: ЛОГИКА ПЕРЕВОДА
# ==========================================
def safe_translate(text, target_lang='ru'):
"""Переводит текст, обходя лимиты по символам."""
translator = GoogleTranslator(source='auto', target=target_lang)
if len(text) <= MAX_TEXT_LEN:
return translator.translate(text)
chunks =[]
current_chunk = ""
for line in text.splitlines(keepends=True):
if len(current_chunk) + len(line) < MAX_TEXT_LEN:
current_chunk += line
else:
if current_chunk.strip():
chunks.append(translator.translate(current_chunk))
time.sleep(API_TIMEOUT)
current_chunk = line
if current_chunk.strip():
chunks.append(translator.translate(current_chunk))
return "".join(chunks)
def detect_language_name(text):
"""Определяет язык и возвращает его красивое название."""
try:
# Убираем лишние знаки для точности определения
import re
clean_text = re.sub(r'[^\w\s]', '', text)
if not clean_text.strip():
return "Неизвестно"
lang_code = detect(clean_text)
for name, code in LANGUAGES_DICT.items():
if code.lower() == lang_code.lower():
return name.capitalize()
return "Неизвестно"
except LangDetectException:
return "Неизвестно"
# ==========================================
# ФРОНТЭНД: СОВРЕМЕННЫЙ UI
# ==========================================
class ModernTranslatorApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("⚡ Быстрый переводчик")
self.geometry("950x550")
self.minsize(700, 450)
# Настройка сетки окна (2 колонки)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(1, weight=1)
self.build_ui()
def build_ui(self):
# --------------------------------------------------
# ВЕРХНЯЯ ПАНЕЛЬ (Заголовки и выбор языка)
# --------------------------------------------------
# Левый заголовок (Исходный язык)
self.frame_top_left = ctk.CTkFrame(self, fg_color="transparent")
self.frame_top_left.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")
self.lbl_source = ctk.CTkLabel(self.frame_top_left, text="Исходный текст", font=("Roboto", 16, "bold"))
self.lbl_source.pack(side="left")
self.lbl_detected = ctk.CTkLabel(self.frame_top_left, text="(Определен: Авто)", text_color=("gray50", "gray70"), font=("Roboto", 12))
self.lbl_detected.pack(side="left", padx=10)
# Правый заголовок (Выбор целевого языка)
self.frame_top_right = ctk.CTkFrame(self, fg_color="transparent")
self.frame_top_right.grid(row=0, column=1, padx=20, pady=(20, 10), sticky="ew")
ctk.CTkLabel(self.frame_top_right, text="Перевод на:", font=("Roboto", 16, "bold")).pack(side="left")
self.target_lang_cb = ctk.CTkComboBox(self.frame_top_right, values=LANGUAGES_LIST, width=200, state="readonly")
self.target_lang_cb.set("Russian") # По умолчанию русский
self.target_lang_cb.pack(side="left", padx=15)
# --------------------------------------------------
# ЦЕНТРАЛЬНАЯ ПАНЕЛЬ (Текстовые поля)
# --------------------------------------------------
# Поле ввода (Слева)
self.textbox_in = ctk.CTkTextbox(self, font=("Roboto", 14), wrap="word", corner_radius=10)
self.textbox_in.grid(row=1, column=0, padx=(20, 10), pady=0, sticky="nsew")
# Поле вывода (Справа)
self.textbox_out = ctk.CTkTextbox(self, font=("Roboto", 14), wrap="word", corner_radius=10, fg_color=("gray90", "gray16"))
self.textbox_out.grid(row=1, column=1, padx=(10, 20), pady=0, sticky="nsew")
self.textbox_out.configure(state="disabled") # Только для чтения
# Подсказка
self.textbox_in.insert("0.0", "Введите текст здесь...\n\n(Подсказка: нажмите Enter для перевода, Shift+Enter для новой строки)")
self.textbox_in.bind("<FocusIn>", self.clear_placeholder)
# Привязка клавиш
self.textbox_in.bind("<Return>", self.handle_enter)
self.textbox_in.bind("<Shift-Return>", self.handle_shift_enter)
# --------------------------------------------------
# НИЖНЯЯ ПАНЕЛЬ (Кнопки-иконки)
# --------------------------------------------------
self.frame_bottom_left = ctk.CTkFrame(self, fg_color="transparent")
self.frame_bottom_left.grid(row=2, column=0, padx=20, pady=15, sticky="ew")
self.frame_bottom_right = ctk.CTkFrame(self, fg_color="transparent")
self.frame_bottom_right.grid(row=2, column=1, padx=20, pady=15, sticky="ew")
# Кнопки слева
self.btn_clear = ctk.CTkButton(self.frame_bottom_left, text="🗑️ Очистить", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.clear_all)
self.btn_clear.pack(side="left")
self.btn_translate = ctk.CTkButton(self.frame_bottom_left, text="Перевести ➔", width=140, font=("Roboto", 14, "bold"), command=self.run_translation)
self.btn_translate.pack(side="right")
# Кнопки справа
self.btn_save = ctk.CTkButton(self.frame_bottom_right, text="💾 Сохранить", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.save_to_file)
self.btn_save.pack(side="right")
self.btn_copy = ctk.CTkButton(self.frame_bottom_right, text="📋 Скопировать", width=120, fg_color=("gray75", "gray30"), hover_color=("gray65", "gray25"), text_color=("black", "white"), command=self.copy_to_clipboard)
self.btn_copy.pack(side="right", padx=10)
# Переменная для контроля плейсхолдера
self.placeholder_cleared = False
# ==========================================
# ЛОГИКА ИНТЕРФЕЙСА
# ==========================================
def clear_placeholder(self, event):
"""Убирает подсказку при первом клике на текстовое поле."""
if not self.placeholder_cleared:
self.textbox_in.delete("0.0", "end")
self.placeholder_cleared = True
def handle_enter(self, event):
"""Срабатывает при нажатии Enter (запускает перевод)."""
self.run_translation()
return "break" # Блокирует создание новой строки
def handle_shift_enter(self, event):
"""Срабатывает при Shift+Enter (создает новую строку)."""
pass # Ничего не делаем, CustomTkinter сам перенесет строку
def clear_all(self):
"""Очищает оба поля."""
self.textbox_in.delete("0.0", "end")
self.textbox_out.configure(state="normal")
self.textbox_out.delete("0.0", "end")
self.textbox_out.configure(state="disabled")
self.lbl_detected.configure(text="(Определен: Авто)")
self.placeholder_cleared = True # Чтобы подсказка не появлялась снова
def save_to_file(self):
"""Сохраняет перевод в .txt файл."""
text = self.textbox_out.get("0.0", "end").strip()
if not text:
messagebox.showinfo("Пусто", "Нет переведенного текста для сохранения.")
return
filepath = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")], title="Сохранить перевод")
if filepath:
try:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(text)
messagebox.showinfo("Успех", "Текст успешно сохранен!")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}")
def copy_to_clipboard(self):
"""Копирует переведенный текст в буфер обмена."""
text = self.textbox_out.get("0.0", "end").strip()
if text:
self.clipboard_clear()
self.clipboard_append(text)
# Меняем текст кнопки на пару секунд для визуала
self.btn_copy.configure(text="✔️ Скопировано!")
self.after(2000, lambda: self.btn_copy.configure(text="📋 Скопировать"))
def run_translation(self):
"""Главная функция перевода (выполняется в фоне)."""
src_text = self.textbox_in.get("0.0", "end").strip()
# Если в окне остался плейсхолдер или пусто
if not src_text or not self.placeholder_cleared:
return
target_name = self.target_lang_cb.get().lower()
target_code = LANGUAGES_DICT.get(target_name, 'ru')
# Блокируем UI на время запроса
self.btn_translate.configure(state="disabled", text="⏳ Перевод...")
self.textbox_out.configure(state="normal")
self.textbox_out.delete("0.0", "end")
self.textbox_out.insert("0.0", "Запрос к серверу перевода...")
self.textbox_out.configure(state="disabled")
def background_task():
# 1. Определяем язык
det_name = detect_language_name(src_text)
# 2. Переводим текст
try:
res = safe_translate(src_text, target_lang=target_code)
except Exception as e:
res = f"⚠️ Ошибка перевода. Проверьте интернет или повторите позже.\n\nДетали: {e}"
# 3. Возвращаем результат в главный поток (UI)
def update_ui():
self.lbl_detected.configure(text=f"(Определен: {det_name})")
self.textbox_out.configure(state="normal")
self.textbox_out.delete("0.0", "end")
self.textbox_out.insert("0.0", res)
self.textbox_out.configure(state="disabled")
self.btn_translate.configure(state="normal", text="Перевести ➔")
self.after(0, update_ui)
# Запускаем поток
threading.Thread(target=background_task, daemon=True).start()
if __name__ == "__main__":
app = ModernTranslatorApp()
app.mainloop()
+45
View File
@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = []
tmp_ret = collect_all('customtkinter')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['fast_translate.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='fast_translate',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+60
View File
@@ -0,0 +1,60 @@
### 📝 Описание программ
#### 1. «Быстрый переводчик» (Modern Translator)
Современное настольное приложение для молниеносного перевода текста. Отлично подходит для ручной работы, тестирования переводов или общения. Построено на базе библиотеки `CustomTkinter`, благодаря чему имеет стильный дизайн, скругленные элементы и автоматически подстраивается под темную или светлую тему вашей операционной системы.
**Главные функции:**
* **Интеллектуальное автоопределение языка:** Программа сама понимает, на каком языке написан исходный текст.
* **Обход лимитов Google:** Если вставить огромный текст, программа незаметно для пользователя разобьет его на части (по 4800 символов), переведет каждую с безопасным таймаутом и склеит обратно. Никаких блокировок от Google!
* **Управление с клавиатуры:** Нажатие `Enter` мгновенно запускает перевод, а `Shift + Enter` делает перенос строки.
* **Удобные инструменты:** Кнопка «Скопировать» для быстрого переноса результата в буфер обмена, кнопка «Очистить» для мгновенного сброса окон и «Сохранить» для экспорта перевода в `.txt` файл.
* **Асинхронность:** Интерфейс не зависает во время ожидания ответа от серверов.
#### 2. «Групповой локализатор кода» (PRO Translator)
Мощный инструмент для массового автоматического перевода проектов (исходного кода). Программа сама сканирует папки, находит файлы исходного кода (Python, QML, JS и др.) и переводит только читаемый текст, не ломая архитектуру программы.
**Главные функции:**
* **Умный парсинг кода:** Использует лексический анализ (`tokenize` для Python) и регулярные выражения, чтобы извлекать текст *только* из кавычек и комментариев, полностью игнорируя системный код (циклы, переменные, функции).
* **Умный фильтр технических строк:** Защищает от перевода системные ключи, пути к файлам (`assets/logo.png`) и переменные (`camelCase`, `snake_case`). Переводятся только осмысленные фразы или иероглифы.
* **Восстановление плейсхолдеров:** Автоматически чинит переменные форматирования (например, возвращает `% s` обратно в `%s`), которые часто портит Google Переводчик.
* **Безопасность (Бэкапы):** Автоматически создает резервные копии оригинальных файлов с расширением `.bak` перед любыми изменениями.
* **Фильтр исходного языка:** Позволяет указать, что переводить нужно *только* с китайского или японского, пропуская английский текст.
* **Детальная статистика:** Визуальный прогресс-бар и журнал логов (Log) в реальном времени показывают, какой файл сейчас обрабатывается и сколько всего переведено.
---
### ⚙️ Инструкция по компиляции в `.exe`
Для превращения `.py` скриптов в `.exe` мы будем использовать самую популярную библиотеку — **PyInstaller**.
#### Шаг 1: Подготовка
Откройте терминал (командную строку `cmd` или PowerShell) и установите PyInstaller:
```bash
pip install pyinstaller
```
#### Шаг 2: Компиляция «Группового переводчика» (PRO Translator)
Перейдите в папку, где лежит ваш скрипт (например, `pro_translator.py`). Выполните следующую команду:
```bash
pyinstaller --noconsole --onefile pro_translator.py
```
* **`--noconsole`** (или `-w`) — скрывает черное окно командной строки, чтобы открывался только красивый графический интерфейс.
* **`--onefile`** (или `-F`) — упаковывает все библиотеки, зависимости и сам Python в один единственный `.exe` файл (его будет удобно переносить).
#### Шаг 3: Компиляция «Быстрого переводчика» (Modern Translator)
Так как этот скрипт использует нестандартную библиотеку отрисовки интерфейса (`customtkinter`), PyInstaller'у нужно явно указать, чтобы он захватил её графические ассеты. Выполните команду:
```bash
pyinstaller --noconsole --onefile --collect-all customtkinter modern_translator.py
```
* **`--collect-all customtkinter`** — гарантирует, что шрифты и темы CustomTkinter будут корректно встроены в `.exe`.
#### Шаг 4: Где найти готовые программы?
После того как консоль закончит работу (это может занять 1–3 минуты), в вашей папке со скриптами появятся новые папки: `build` и `dist`.
1. Зайдите в папку **`dist`**.
2. Именно там лежат ваши готовые `modern_translator.exe` и `pro_translator.exe`.
3. Теперь эти файлы можно копировать, переносить на флешке и запускать на других компьютерах с Windows. Папки `build` и файлы `.spec` можно удалить, они нужны были только для сборки.
> 💡 **Важное замечание про Антивирусы:**
> Windows Defender или другие антивирусы иногда могут ругаться на `.exe` файлы, созданные через `PyInstaller` (выдавать предупреждение "Неизвестный издатель"). Это абсолютно нормальная реакция системы на программы без платной цифровой подписи разработчика. Просто нажмите "Подробнее" -> "Выполнить в любом случае" или добавьте файл в исключения.
+548
View File
@@ -0,0 +1,548 @@
import os
import re
import tokenize
import io
import time
import random
import threading
import textwrap
import shutil
import urllib.request
import json
import customtkinter as ctk
from tkinter import filedialog, messagebox
from deep_translator import GoogleTranslator
from deep_translator.exceptions import RequestError
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
class TranslationWorker(threading.Thread):
def __init__(self, path, is_file, recursive, extensions, target_lang, only_cjk, save_as_new, create_bak, log_callback, progress_callback, finish_callback, engine="Google Translate", ollama_model=""):
super().__init__()
self.path = path
self.is_file = is_file
self.recursive = recursive
self.extensions =[ext.strip().lower() for ext in extensions.split(',')]
self.only_cjk = only_cjk
self.save_as_new = save_as_new
self.create_bak = create_bak
self.engine = engine
self.ollama_model = ollama_model
self.target_lang = target_lang
# Маппинг кодов языков в их английские названия для промпта Ollama
self.lang_map = {
"ru": "Russian",
"en": "English"
}
self.log = log_callback
self.update_progress = progress_callback
self.finish = finish_callback
self.translator = GoogleTranslator(source='auto', target=self.target_lang)
self.request_count = 0
self.is_running = True
def run(self):
self.log("Начало сканирования...")
files_to_process =[]
# Защита от зацикливания: игнорируем файлы, которые уже являются переведенными копиями
suffix = f"_{self.target_lang}"
if self.is_file:
files_to_process.append(self.path)
else:
if self.recursive:
for root, _, files in os.walk(self.path):
for file in files:
if any(file.lower().endswith(ext) for ext in self.extensions):
# Пропускаем уже переведенные файлы
name, ext = os.path.splitext(file)
if not name.endswith(suffix):
files_to_process.append(os.path.join(root, file))
else:
for file in os.listdir(self.path):
if os.path.isfile(os.path.join(self.path, file)):
if any(file.lower().endswith(ext) for ext in self.extensions):
name, ext = os.path.splitext(file)
if not name.endswith(suffix):
files_to_process.append(os.path.join(self.path, file))
total_files = len(files_to_process)
self.log(f"Найдено файлов для обработки: {total_files}")
if total_files == 0:
self.finish()
return
for idx, filepath in enumerate(files_to_process):
if not self.is_running:
self.log("Процесс остановлен пользователем.")
break
self.log(f"Обработка: {os.path.basename(filepath)}")
try:
if filepath.endswith('.py'):
self.process_python_file(filepath)
elif filepath.endswith(('.qml', '.js', '.json', '.cpp', '.cs')):
self.process_regex_file(filepath)
else:
self.process_plain_text(filepath)
except Exception as e:
self.log(f"Ошибка в файле {os.path.basename(filepath)}: {str(e)}")
self.update_progress((idx + 1) / total_files)
self.log("Перевод завершен!")
self.finish()
def stop(self):
self.is_running = False
def check_limits(self):
self.request_count += 1
time.sleep(random.uniform(0.5, 1.5))
if self.request_count % 50 == 0:
self.log("Пауза 5 сек во избежание блокировки Google API...")
time.sleep(random.uniform(5.0, 8.0))
def has_target_chars(self, text):
if not self.only_cjk:
return re.search(r'[a-zA-Zа-яА-Я\u4e00-\u9fff\u3040-\u30ff]', text) is not None
return bool(re.search(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf]', text))
def fix_orthography(self, text):
if not text: return text
text = re.sub(r'%\s+([a-zA-Z0-9])', r'%\1', text)
text = re.sub(r'\{\s*(.*?)\s*\}', r'{\1}', text)
text = re.sub(r'\\ \s*n', r'\\n', text)
text = re.sub(r'\\ \s*t', r'\\t', text)
text = text.replace('...', '')
return text
def translate_with_ollama(self, text):
url = "http://localhost:11434/api/chat"
lang_name = self.lang_map.get(self.target_lang, "Russian")
system_prompt = (
f"You are a professional translator. Your task is to translate any text provided by the user into {lang_name}. "
"CRITICAL RULES: "
f"1. Output ONLY the translated {lang_name} text. "
"2. DO NOT add any introductions, notes, reasoning, or explanations. "
"3. Maintain exactly the original formatting, punctuation, line breaks, and special characters. "
"4. DO NOT translate variables or code syntax, translate only the readable text content."
)
data = {
"model": self.ollama_model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"stream": False,
"options": {
"temperature": 0.1,
"num_ctx": 8192
}
}
# Делаем 2 попытки на случай сбоя Ollama
for attempt in range(2):
try:
# Увеличен таймаут до 600 секунд (10 минут) для медленных генераций или инициализации модели в VRAM
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=600) as response:
result = json.loads(response.read().decode('utf-8'))
translated = result.get('message', {}).get('content', '').strip()
if translated:
return translated
except Exception as e:
if attempt == 1:
self.log(f"Ошибка Ollama (модель '{self.ollama_model}'): {e}")
time.sleep(2)
return text
def process_chunk(self, chunk):
"""Единый метод перевода для одного куска (чанка) текста"""
if self.engine == "Ollama":
return self.translate_with_ollama(chunk)
else:
self.check_limits()
return self.translator.translate(chunk)
def translate_text(self, text):
if not text.strip() or not self.has_target_chars(text):
return text
try:
# Для Google Translate мы можем отправлять куски по 4000 символов
# Для Ollama куски нужно делать меньше (~1500 символов), чтобы избежать таймаутов,
# потери контекста и ускорить пошаговую генерацию.
max_chunk_size = 4000 if self.engine == "Google Translate" else 1500
if len(text) > max_chunk_size:
chunks = textwrap.wrap(text, max_chunk_size, break_long_words=False, replace_whitespace=False)
translated_chunks = []
for chunk in chunks:
res = self.process_chunk(chunk)
translated_chunks.append(res)
translated = " ".join(translated_chunks)
else:
translated = self.process_chunk(text)
return self.fix_orthography(translated)
except RequestError:
# Отрабатывает только при обрыве сети Google Translate
if self.engine == "Google Translate":
self.log("Ошибка сети Google. Повторная попытка через 3 сек...")
time.sleep(3)
return self.translate_text(text)
return text
except Exception as e:
self.log(f"Ошибка перевода: {e}")
return text
def get_save_path(self, filepath):
if self.save_as_new:
base, ext = os.path.splitext(filepath)
return f"{base}_{self.target_lang}{ext}"
return filepath
def handle_backup(self, filepath):
if self.create_bak and not self.save_as_new:
shutil.copy2(filepath, filepath + ".bak")
def process_python_file(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)
replacements =[]
for tok in tokens:
if tok.type in (tokenize.STRING, tokenize.COMMENT) and self.has_target_chars(tok.string):
if tok.type == tokenize.COMMENT:
m = re.match(r'^(#\s*)(.*)', tok.string, re.DOTALL)
if m:
prefix, content = m.groups()
replacements.append((tok.start, tok.end, prefix + self.translate_text(content)))
elif tok.type == tokenize.STRING:
m = re.match(r'^([a-zA-Z]*)("{1,3}|\'{1,3})(.*)(\2)$', tok.string, re.DOTALL)
if m:
prefix, quote, content, _ = m.groups()
translated = self.translate_text(content)
if 'r' not in prefix.lower() and len(quote) == 1:
translated = translated.replace(quote, f"\\{quote}")
replacements.append((tok.start, tok.end, f"{prefix}{quote}{translated}{quote}"))
if replacements:
self.handle_backup(filepath)
self._apply_replacements(filepath, self.get_save_path(filepath), source, replacements)
def process_regex_file(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
pattern = re.compile(
r'(/\*.*?\*/)|(//.*?$)|("(?:\\.|[^"\\])*")|(\'(?:\\.|[^\'\\])*\')|(`(?:\\.|[^`\\])*`)',
re.DOTALL | re.MULTILINE
)
replacements =[]
for match in pattern.finditer(source):
text = match.group(0)
if self.has_target_chars(text):
if text.startswith('/*'):
replacements.append((match.start(), match.end(), f"/*{self.translate_text(text[2:-2])}*/"))
elif text.startswith('//'):
replacements.append((match.start(), match.end(), f"//{self.translate_text(text[2:])}"))
else:
quote = text[0]
content = text[1:-1]
translated = self.translate_text(content).replace(quote, f"\\{quote}")
replacements.append((match.start(), match.end(), f"{quote}{translated}{quote}"))
if replacements:
self.handle_backup(filepath)
for start, end, new_text in reversed(replacements):
source = source[:start] + new_text + source[end:]
with open(self.get_save_path(filepath), 'w', encoding='utf-8') as f:
f.write(source)
def process_plain_text(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
if self.has_target_chars(source):
self.handle_backup(filepath)
translated = self.translate_text(source)
with open(self.get_save_path(filepath), 'w', encoding='utf-8') as f:
f.write(translated)
def _apply_replacements(self, original_filepath, save_filepath, source, replacements):
lines = source.splitlines(keepends=True)
for start, end, new_text in reversed(replacements):
start_row, start_col = start[0] - 1, start[1]
end_row, end_col = end[0] - 1, end[1]
if start_row == end_row:
lines[start_row] = lines[start_row][:start_col] + new_text + lines[start_row][end_col:]
else:
lines[start_row] = lines[start_row][:start_col] + new_text
for r in range(start_row + 1, end_row):
lines[r] = ""
lines[end_row] = lines[end_row][end_col:]
with open(save_filepath, 'w', encoding='utf-8') as f:
f.write("".join(lines))
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Code & Text Translator Pro")
self.geometry("900x700")
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=2)
self.grid_rowconfigure(0, weight=1)
self.worker = None
self.selected_path = ""
self.is_file_mode = False
self._build_ui()
self.refresh_ollama_models()
def get_ollama_models(self):
try:
req = urllib.request.Request("http://localhost:11434/api/tags")
with urllib.request.urlopen(req, timeout=2) as response:
data = json.loads(response.read().decode('utf-8'))
models = [m['name'] for m in data.get('models', [])]
return models if models else ["Нет доступных моделей"]
except Exception:
return ["Ollama не запущена"]
def refresh_ollama_models(self):
models = self.get_ollama_models()
self.opt_ollama.configure(values=models)
if models and models[0] not in ["Нет доступных моделей", "Ollama не запущена"]:
self.opt_ollama.set(models[0])
else:
self.opt_ollama.set(models[0] if models else "")
def _build_ui(self):
# Левая панель (Настройки)
self.frame_settings = ctk.CTkScrollableFrame(self)
self.frame_settings.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
ctk.CTkLabel(self.frame_settings, text="Настройки", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(0, 10))
# Кнопки выбора
frame_btns = ctk.CTkFrame(self.frame_settings, fg_color="transparent")
frame_btns.pack(fill="x", pady=5, padx=5)
self.btn_folder = ctk.CTkButton(frame_btns, text="Выбрать папку", command=self.select_folder)
self.btn_folder.pack(side="left", fill="x", expand=True, padx=(0, 2))
self.btn_file = ctk.CTkButton(frame_btns, text="Выбрать файл", command=self.select_file)
self.btn_file.pack(side="right", fill="x", expand=True, padx=(2, 0))
self.lbl_path = ctk.CTkLabel(self.frame_settings, text="Ничего не выбрано", text_color="gray", wraplength=250)
self.lbl_path.pack(pady=5, padx=10)
# Режимы сохранения
ctk.CTkLabel(self.frame_settings, text="Безопасность:", font=ctk.CTkFont(weight="bold")).pack(pady=(15, 0), anchor="w", padx=10)
self.var_new_file = ctk.BooleanVar(value=True)
self.chk_new_file = ctk.CTkCheckBox(self.frame_settings, text="Создать новый файл (суффикс языка)", variable=self.var_new_file, command=self.toggle_backup_chk)
self.chk_new_file.pack(pady=5, padx=10, anchor="w")
self.var_bak = ctk.BooleanVar(value=False)
self.chk_bak = ctk.CTkCheckBox(self.frame_settings, text="Создавать бэкап (.bak)", variable=self.var_bak, state="disabled")
self.chk_bak.pack(pady=5, padx=10, anchor="w")
# Чекбокс подпапок
self.var_recursive = ctk.BooleanVar(value=True)
self.chk_recursive = ctk.CTkCheckBox(self.frame_settings, text="Включая подпапки", variable=self.var_recursive)
self.chk_recursive.pack(pady=(15, 5), padx=10, anchor="w")
# Расширения
ctk.CTkLabel(self.frame_settings, text="Расширения файлов (через запятую):").pack(pady=(10, 0), padx=10, anchor="w")
self.ent_exts = ctk.CTkEntry(self.frame_settings, placeholder_text=".py, .qml, .txt")
self.ent_exts.insert(0, ".py, .qml, .txt, .json")
self.ent_exts.pack(pady=5, padx=10, fill="x")
# Выбор системы перевода
ctk.CTkLabel(self.frame_settings, text="Система перевода:").pack(pady=(10, 0), padx=10, anchor="w")
self.opt_engine = ctk.CTkOptionMenu(self.frame_settings, values=["Google Translate", "Ollama"], command=self.toggle_engine)
self.opt_engine.pack(pady=5, padx=10, fill="x")
# Блок Ollama
frame_ollama = ctk.CTkFrame(self.frame_settings, fg_color="transparent")
frame_ollama.pack(fill="x", pady=(5, 0), padx=5)
self.lbl_ollama = ctk.CTkLabel(frame_ollama, text="Модель Ollama:")
self.lbl_ollama.pack(side="left", padx=5)
self.btn_refresh_models = ctk.CTkButton(frame_ollama, text="🔄 Обновить", width=80, command=self.refresh_ollama_models)
self.btn_refresh_models.pack(side="right", padx=5)
self.opt_ollama = ctk.CTkOptionMenu(self.frame_settings, values=["Загрузка..."])
self.opt_ollama.pack(pady=5, padx=10, fill="x")
# Язык
self.lbl_lang = ctk.CTkLabel(self.frame_settings, text="Целевой язык:")
self.lbl_lang.pack(pady=(10, 0), padx=10, anchor="w")
self.opt_lang = ctk.CTkOptionMenu(self.frame_settings, values=["ru (Русский)", "en (Английский)", "uk (Украинский)"])
self.opt_lang.set("ru (Русский)") # По умолчанию русский язык
self.opt_lang.pack(pady=5, padx=10, fill="x")
# Инициализация интерфейса (скрытие/блокировка лишних полей)
self.toggle_engine("Google Translate")
# Ограничение по CJK
self.var_cjk = ctk.BooleanVar(value=True)
self.chk_cjk = ctk.CTkCheckBox(self.frame_settings, text="Только Китайский/Японский", variable=self.var_cjk)
self.chk_cjk.pack(pady=10, padx=10, anchor="w")
# Пространство, чтобы кнопки всегда были внизу
ctk.CTkLabel(self.frame_settings, text="").pack(expand=True, fill="both")
# Кнопки управления
self.btn_start = ctk.CTkButton(self.frame_settings, text="▶ НАЧАТЬ ПЕРЕВОД", fg_color="green", hover_color="darkgreen", command=self.start_translation)
self.btn_start.pack(pady=(15, 5), padx=10, fill="x")
self.btn_stop = ctk.CTkButton(self.frame_settings, text="ОСТАНОВИТЬ", fg_color="red", hover_color="darkred", state="disabled", command=self.stop_translation)
self.btn_stop.pack(pady=5, padx=10, fill="x")
# Правая панель (Логи и Прогресс)
self.frame_logs = ctk.CTkFrame(self)
self.frame_logs.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
self.frame_logs.grid_rowconfigure(1, weight=1)
self.frame_logs.grid_columnconfigure(0, weight=1)
ctk.CTkLabel(self.frame_logs, text="Журнал работы:", font=ctk.CTkFont(size=14, weight="bold")).grid(row=0, column=0, pady=5, padx=10, sticky="w")
self.txt_log = ctk.CTkTextbox(self.frame_logs, state="disabled", wrap="word", font=ctk.CTkFont(family="Consolas", size=12))
self.txt_log.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
self.progress_bar = ctk.CTkProgressBar(self.frame_logs)
self.progress_bar.grid(row=2, column=0, padx=10, pady=15, sticky="ew")
self.progress_bar.set(0)
def toggle_engine(self, choice):
if choice == "Ollama":
self.opt_ollama.configure(state="normal")
self.btn_refresh_models.configure(state="normal")
else:
self.opt_ollama.configure(state="disabled")
self.btn_refresh_models.configure(state="disabled")
def toggle_backup_chk(self):
"""Если включено создание нового файла, бэкап не нужен."""
if self.var_new_file.get():
self.var_bak.set(False)
self.chk_bak.configure(state="disabled")
else:
self.chk_bak.configure(state="normal")
def select_folder(self):
folder = filedialog.askdirectory()
if folder:
self.lbl_path.configure(text=folder)
self.selected_path = folder
self.is_file_mode = False
self.chk_recursive.configure(state="normal")
def select_file(self):
file = filedialog.askopenfilename(filetypes=[("All Supported Files", "*.*")])
if file:
self.lbl_path.configure(text=file)
self.selected_path = file
self.is_file_mode = True
self.chk_recursive.configure(state="disabled")
def log(self, message):
def update():
self.txt_log.configure(state="normal")
self.txt_log.insert("end", message + "\n")
self.txt_log.see("end")
self.txt_log.configure(state="disabled")
self.after(0, update)
def update_progress(self, value):
self.after(0, lambda: self.progress_bar.set(value))
def start_translation(self):
if not self.selected_path:
messagebox.showwarning("Внимание", "Сначала выберите файл или папку!")
return
target_lang = self.opt_lang.get().split(" ")[0]
extensions = self.ent_exts.get()
if not extensions and not self.is_file_mode:
messagebox.showwarning("Внимание", "Укажите хотя бы одно расширение!")
return
# Блокировка UI
self.btn_start.configure(state="disabled")
self.btn_folder.configure(state="disabled")
self.btn_file.configure(state="disabled")
self.chk_new_file.configure(state="disabled")
self.chk_bak.configure(state="disabled")
self.btn_stop.configure(state="normal")
self.opt_engine.configure(state="disabled")
self.opt_ollama.configure(state="disabled")
self.btn_refresh_models.configure(state="disabled")
self.opt_lang.configure(state="disabled")
self.progress_bar.set(0)
self.txt_log.configure(state="normal")
self.txt_log.delete("1.0", "end")
self.txt_log.configure(state="disabled")
self.worker = TranslationWorker(
path=self.selected_path,
is_file=self.is_file_mode,
recursive=self.var_recursive.get(),
extensions=extensions,
target_lang=target_lang,
only_cjk=self.var_cjk.get(),
save_as_new=self.var_new_file.get(),
create_bak=self.var_bak.get(),
log_callback=self.log,
progress_callback=self.update_progress,
finish_callback=self.translation_finished,
engine=self.opt_engine.get(),
ollama_model=self.opt_ollama.get()
)
self.worker.start()
def stop_translation(self):
if self.worker:
self.worker.stop()
self.log("Остановка процессов... Пожалуйста, подождите завершения текущего файла.")
self.btn_stop.configure(state="disabled")
def translation_finished(self):
def update():
self.btn_start.configure(state="normal")
self.btn_folder.configure(state="normal")
self.btn_file.configure(state="normal")
self.chk_new_file.configure(state="normal")
self.opt_engine.configure(state="normal")
self.opt_lang.configure(state="normal")
self.toggle_engine(self.opt_engine.get()) # Восстанавливаем состояние полей
self.toggle_backup_chk() # Восстанавливаем состояние чекбокса бэкапов
self.btn_stop.configure(state="disabled")
self.after(0, update)
if __name__ == "__main__":
app = App()
app.mainloop()
+492
View File
@@ -0,0 +1,492 @@
import os
import re
import tokenize
import io
import time
import shutil
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from deep_translator import GoogleTranslator
from langdetect import detect, LangDetectException
# Получаем все поддерживаемые языки от Google Translate
LANGUAGES_DICT = GoogleTranslator().get_supported_languages(as_dict=True)
LANGUAGES_LIST =[lang.capitalize() for lang in LANGUAGES_DICT.keys()]
# Настройки для обхода банов
API_TIMEOUT = 1.5
MAX_TEXT_LEN = 4800
# ==========================================
# БЭКЭНД: ФУНКЦИИ ПЕРЕВОДА И ПАРСИНГА
# ==========================================
def fix_placeholders(text):
"""Восстанавливает переменные форматирования кода после перевода."""
if not text: return text
text = re.sub(r'%\s+([a-zA-Z])', r'%\1', text)
text = re.sub(r'%\s+([0-9]+)', r'%\1', text)
text = re.sub(r'\{\s*(.*?)\s*\}', r'{\1}', text)
return text
def safe_translate(text, target_lang='ru', source_lang='auto'):
"""Переводит текст с разбивкой на части для обхода лимитов Google."""
translator = GoogleTranslator(source=source_lang, target=target_lang)
if len(text) <= MAX_TEXT_LEN:
return translator.translate(text)
chunks =[]
current_chunk = ""
for line in text.splitlines(keepends=True):
if len(current_chunk) + len(line) < MAX_TEXT_LEN:
current_chunk += line
else:
if current_chunk.strip():
chunks.append(translator.translate(current_chunk))
time.sleep(API_TIMEOUT)
current_chunk = line
if current_chunk.strip():
chunks.append(translator.translate(current_chunk))
return "".join(chunks)
def detect_language(text):
"""Определяет язык текста локально (без запросов к API)."""
try:
clean_text = re.sub(r'[^\w\s]', '', text)
if not clean_text.strip(): return None
return detect(clean_text)
except LangDetectException:
return None
def should_translate(content, config):
"""
ГЛАВНЫЙ ФИЛЬТР: Решает, является ли текст системным кодом или интерфейсом.
"""
clean_content = content.strip()
if not clean_content or not any(c.isalpha() for c in clean_content):
return False
if config.get('skip_technical', True):
# Если в строке НЕТ пробелов и НЕТ иероглифов/кириллицы -> это тех. строка
if " " not in clean_content and not re.search(r'[А-Яа-яЁё\u4e00-\u9fff\u3040-\u30ff]', clean_content):
return False
if config['translate_only']:
lang_code = detect_language(clean_content)
if lang_code and lang_code.lower() in config['selected_codes']:
return True
return False
return True
def process_file(filepath, config, update_log_cb):
"""Универсальная функция парсинга."""
try:
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
except UnicodeDecodeError:
return "error_utf8"
replacements =[]
if filepath.endswith('.py'):
tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)
for tok in tokens:
if tok.type in (tokenize.STRING, tokenize.COMMENT):
if tok.type == tokenize.COMMENT:
m = re.match(r'^(#\s*)(.*)', tok.string, re.DOTALL)
if m:
prefix, content = m.groups()
if should_translate(content, config):
translated = safe_translate(content, target_lang='ru')
translated = fix_placeholders(translated)
replacements.append((tok.start, tok.end, prefix + translated))
time.sleep(API_TIMEOUT)
elif tok.type == tokenize.STRING:
m = re.match(r'^([a-zA-Z]*)("{1,3}|\'{1,3})(.*)(\2)$', tok.string, re.DOTALL)
if m:
prefix, quote, content, _ = m.groups()
if should_translate(content, config):
translated = safe_translate(content, target_lang='ru')
translated = fix_placeholders(translated)
if 'r' not in prefix.lower() and len(quote) == 1:
if quote == '"': translated = translated.replace('"', '\\"')
elif quote == "'": translated = translated.replace("'", "\\'")
replacements.append((tok.start, tok.end, f"{prefix}{quote}{translated}{quote}"))
time.sleep(API_TIMEOUT)
else:
pattern = re.compile(
r'(/\*.*?\*/)|'
r'(//.*?$)|'
r'("(?:\\.|[^"\\])*")|'
r"('(?:\\.|[^\'\\])*')|"
r'(`(?:\\.|[^`\\])*`)',
re.DOTALL | re.MULTILINE
)
for match in pattern.finditer(source):
text = match.group(0)
is_comment_multi = text.startswith('/*')
is_comment_single = text.startswith('//')
if is_comment_multi: content = text[2:-2]
elif is_comment_single: content = text[2:]
else: content = text[1:-1]
if should_translate(content, config):
translated = safe_translate(content, target_lang='ru')
translated = fix_placeholders(translated)
if is_comment_multi: new_text = f"/*{translated}*/"
elif is_comment_single: new_text = f"//{translated}"
else:
quote = text[0]
if quote == '"': translated = translated.replace('"', '\\"')
elif quote == "'": translated = translated.replace("'", "\\'")
elif quote == "`": translated = translated.replace("`", "\\`")
new_text = f"{quote}{translated}{quote}"
replacements.append((match.start(), match.end(), new_text))
time.sleep(API_TIMEOUT)
if not replacements:
return "skipped"
if config['backup_enabled']:
shutil.copy2(filepath, filepath + ".bak")
if filepath.endswith('.py'):
lines = source.splitlines(keepends=True)
for start, end, new_text in reversed(replacements):
start_row, start_col = start[0] - 1, start[1]
end_row, end_col = end[0] - 1, end[1]
if start_row == end_row:
lines[start_row] = lines[start_row][:start_col] + new_text + lines[start_row][end_col:]
else:
lines[start_row] = lines[start_row][:start_col] + new_text
for r in range(start_row + 1, end_row): lines[r] = ""
lines[end_row] = lines[end_row][end_col:]
final_source = "".join(lines)
else:
for start, end, new_text in reversed(replacements):
source = source[:start] + new_text + source[end:]
final_source = source
with open(filepath, 'w', encoding='utf-8') as f:
f.write(final_source)
return "translated"
# ==========================================
# ФРОНТЭНД: ГРАФИЧЕСКИЙ ИНТЕРФЕЙС (TKINTER)
# ==========================================
class TranslatorApp:
def __init__(self, root):
self.root = root
self.root.title("PRO Локализатор Кода (Безопасный режим)")
self.root.geometry("750x700")
self.root.minsize(650, 650)
style = ttk.Style()
if 'clam' in style.theme_names():
style.theme_use('clam')
self.build_ui()
def build_ui(self):
# --- ВЕРХНЯЯ ПАНЕЛЬ ---
top_frame = ttk.Frame(self.root, padding=10)
top_frame.pack(fill=tk.X)
ttk.Label(top_frame, text="Настройки безопасного перевода", font=("Arial", 12, "bold")).pack(side=tk.LEFT)
ttk.Button(top_frame, text="⚡ Быстрый перевод", command=self.open_quick_translate).pack(side=tk.RIGHT)
# --- ГЛАВНАЯ ПАНЕЛЬ ---
main_frame = ttk.LabelFrame(self.root, padding=15)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 1. Выбор папки
f_folder = ttk.Frame(main_frame)
f_folder.pack(fill=tk.X, pady=5)
ttk.Label(f_folder, text="Папка с кодом:").pack(side=tk.LEFT, padx=(0,5))
self.folder_var = tk.StringVar()
ttk.Entry(f_folder, textvariable=self.folder_var, state='readonly').pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Button(f_folder, text="Обзор...", command=self.browse_folder).pack(side=tk.LEFT, padx=(5,0))
# 2. Расширения файлов
f_ext = ttk.Frame(main_frame)
f_ext.pack(fill=tk.X, pady=5)
ttk.Label(f_ext, text="Расширения файлов:").pack(side=tk.LEFT, padx=(0,5))
self.ext_var = tk.StringVar(value=".py, .qml, .js")
ttk.Entry(f_ext, textvariable=self.ext_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Label(f_ext, text="(через запятую)", foreground="gray").pack(side=tk.LEFT, padx=5)
# 3. Чекбоксы безопасности
self.backup_var = tk.BooleanVar(value=True)
ttk.Checkbutton(main_frame, text="Создавать .bak файлы оригиналов перед переводом", variable=self.backup_var).pack(anchor=tk.W, pady=(10,0))
self.skip_tech_var = tk.BooleanVar(value=True)
ttk.Checkbutton(main_frame, text="🛡️ Умный фильтр: НЕ переводить технические строки (ключи, пути, camelCase)", variable=self.skip_tech_var).pack(anchor=tk.W, pady=5)
# 4. Настройки языков источника
f_lang = ttk.Frame(main_frame)
f_lang.pack(fill=tk.X, pady=5)
self.translate_only_var = tk.BooleanVar(value=False)
cb_only = ttk.Checkbutton(f_lang, text="Переводить ТОЛЬКО с выбранных языков (иначе переведет всё)",
variable=self.translate_only_var, command=self.toggle_lang_list)
cb_only.pack(anchor=tk.W)
self.langs_listbox = tk.Listbox(f_lang, selectmode=tk.MULTIPLE, height=5, exportselection=False)
self.langs_listbox.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=5)
scrollbar = ttk.Scrollbar(f_lang, orient="vertical", command=self.langs_listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
self.langs_listbox.config(yscrollcommand=scrollbar.set)
for lang in LANGUAGES_LIST:
self.langs_listbox.insert(tk.END, lang)
self.langs_listbox.config(state=tk.DISABLED)
# Прогресс
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill=tk.X, pady=15)
self.lbl_stats = ttk.Label(main_frame, text="Ожидание...")
self.lbl_stats.pack(anchor=tk.W)
# Кнопка старта
self.btn_start = ttk.Button(main_frame, text="▶ Начать массовый перевод", command=self.start_translation)
self.btn_start.pack(fill=tk.X, pady=10)
# Логи
self.txt_log = scrolledtext.ScrolledText(main_frame, height=8, state=tk.DISABLED, bg="#f4f4f4")
self.txt_log.pack(fill=tk.BOTH, expand=True)
def toggle_lang_list(self):
state = tk.NORMAL if self.translate_only_var.get() else tk.DISABLED
self.langs_listbox.config(state=state)
def browse_folder(self):
folder = filedialog.askdirectory()
if folder: self.folder_var.set(folder)
def log(self, text):
def append():
self.txt_log.config(state=tk.NORMAL)
self.txt_log.insert(tk.END, text + "\n")
self.txt_log.see(tk.END)
self.txt_log.config(state=tk.DISABLED)
self.root.after(0, append)
def start_translation(self):
target_dir = self.folder_var.get()
if not target_dir: return messagebox.showwarning("Внимание", "Укажите папку проекта!")
ext_str = self.ext_var.get()
exts = tuple(e.strip().lower() for e in ext_str.split(',') if e.strip())
if not exts: return messagebox.showwarning("Внимание", "Укажите хотя бы одно расширение!")
selected_langs =[]
if self.translate_only_var.get():
indices = self.langs_listbox.curselection()
if not indices:
return messagebox.showwarning("Внимание", "Вы не выбрали ни одного языка в списке!")
for i in indices:
lang_name = self.langs_listbox.get(i).lower()
lang_code = LANGUAGES_DICT.get(lang_name)
if lang_code: selected_langs.append(lang_code.lower())
config = {
'target_dir': target_dir,
'exts': exts,
'backup_enabled': self.backup_var.get(),
'skip_technical': self.skip_tech_var.get(),
'translate_only': self.translate_only_var.get(),
'selected_codes': selected_langs
}
self.btn_start.config(state=tk.DISABLED)
self.txt_log.config(state=tk.NORMAL)
self.txt_log.delete(1.0, tk.END)
self.txt_log.config(state=tk.DISABLED)
threading.Thread(target=self.worker, args=(config,), daemon=True).start()
def worker(self, config):
self.log("[i] Сканирование файлов...")
target_files =[]
for root, _, files in os.walk(config['target_dir']):
for file in files:
if file.lower().endswith(config['exts']):
target_files.append(os.path.join(root, file))
total = len(target_files)
if total == 0:
self.log("[!] Файлы с указанными расширениями не найдены.")
self.root.after(0, lambda: self.btn_start.config(state=tk.NORMAL))
return
translated_c = skipped_c = err_c = 0
for i, filepath in enumerate(target_files, 1):
short_p = os.path.relpath(filepath, config['target_dir'])
self.root.after(0, lambda v=i, t=total: self.progress_var.set(v / t * 100))
self.root.after(0, lambda s=short_p: self.lbl_stats.config(text=f"Обработка: {s}"))
res = process_file(filepath, config, self.log)
if res == "translated":
translated_c += 1
self.log(f"[+] Переведено: {short_p}")
elif res == "skipped":
skipped_c += 1
else:
err_c += 1
self.log(f"[!] Ошибка кодировки: {short_p}")
self.log("="*40)
self.log(f"ГОТОВО! Переведено: {translated_c} | Пропущено: {skipped_c} | Ошибок: {err_c}")
self.root.after(0, lambda: self.lbl_stats.config(text="Работа завершена."))
self.root.after(0, lambda: self.btn_start.config(state=tk.NORMAL))
# ==========================================
# ОКНО БЫСТРОГО ПЕРЕВОДА
# ==========================================
def open_quick_translate(self):
qt_win = tk.Toplevel(self.root)
qt_win.title("⚡ Быстрый перевод")
qt_win.geometry("850x550")
qt_win.minsize(650, 450)
# --- Верхняя панель языков ---
top_f = ttk.Frame(qt_win, padding=10)
top_f.pack(fill=tk.X)
ttk.Label(top_f, text="Целевой язык:").pack(side=tk.LEFT)
target_lang_cb = ttk.Combobox(top_f, values=LANGUAGES_LIST, state="readonly", width=20)
target_lang_cb.set("Russian")
target_lang_cb.pack(side=tk.LEFT, padx=10)
self.lbl_detected = ttk.Label(top_f, text="Исходный: Автоопределение", foreground="blue")
self.lbl_detected.pack(side=tk.RIGHT)
# --- Разделитель текстовых панелей ---
paned = ttk.PanedWindow(qt_win, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
left_f = ttk.Frame(paned)
right_f = ttk.Frame(paned)
paned.add(left_f, weight=1)
paned.add(right_f, weight=1)
# -------------------------------------
# ЛЕВАЯ ЧАСТЬ (ВВОД + ИКОНКА ОЧИСТКИ)
# -------------------------------------
left_header = ttk.Frame(left_f)
left_header.pack(fill=tk.X, pady=(0, 2))
ttk.Label(left_header, text="Вставьте текст (Enter - перевод):").pack(side=tk.LEFT)
def clear_fields():
txt_in.delete(1.0, tk.END)
txt_out.config(state=tk.NORMAL)
txt_out.delete(1.0, tk.END)
txt_out.config(state=tk.DISABLED)
# Кнопка-иконка Очистки (Корзина)
btn_clear = ttk.Button(left_header, text="🗑️ Очистить", command=clear_fields)
btn_clear.pack(side=tk.RIGHT)
txt_in = scrolledtext.ScrolledText(left_f, wrap=tk.WORD, font=("Arial", 11))
txt_in.pack(fill=tk.BOTH, expand=True)
# -------------------------------------
# ПРАВАЯ ЧАСТЬ (ВЫВОД + ИКОНКА СОХРАНЕНИЯ)
# -------------------------------------
right_header = ttk.Frame(right_f)
right_header.pack(fill=tk.X, pady=(0, 2))
ttk.Label(right_header, text="Перевод:").pack(side=tk.LEFT)
def save_to_file():
text = txt_out.get(1.0, tk.END).strip()
if not text: return
filepath = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
if filepath:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(text)
messagebox.showinfo("Сохранено", "Текст успешно сохранен в файл!")
# Кнопка-иконка Сохранения (Дискета)
btn_save = ttk.Button(right_header, text="💾 Сохранить", command=save_to_file)
btn_save.pack(side=tk.RIGHT)
txt_out = scrolledtext.ScrolledText(right_f, wrap=tk.WORD, font=("Arial", 11), bg="#f9f9f9")
txt_out.pack(fill=tk.BOTH, expand=True)
# --- Нижняя панель (Только кнопка старта перевода) ---
bot_f = ttk.Frame(qt_win, padding=10)
bot_f.pack(fill=tk.X)
def run_translation(event=None):
src_text = txt_in.get(1.0, tk.END).strip()
if not src_text:
return "break" if event else None
target_name = target_lang_cb.get().lower()
target_code = LANGUAGES_DICT.get(target_name, 'ru')
btn_trans.config(state=tk.DISABLED, text="Перевод...")
txt_out.config(state=tk.NORMAL)
txt_out.delete(1.0, tk.END)
txt_out.insert(tk.END, "Запрос к серверу...")
txt_out.config(state=tk.DISABLED)
def task():
det_code = detect_language(src_text)
det_name = "Неизвестно"
if det_code:
for name, code in LANGUAGES_DICT.items():
if code.lower() == det_code.lower():
det_name = name.capitalize()
break
try:
res = safe_translate(src_text, target_lang=target_code, source_lang='auto')
except Exception as e:
res = f"Ошибка: {e}"
def update_ui():
self.lbl_detected.config(text=f"Определен язык: {det_name}")
txt_out.config(state=tk.NORMAL)
txt_out.delete(1.0, tk.END)
txt_out.insert(tk.END, res)
txt_out.config(state=tk.DISABLED)
btn_trans.config(state=tk.NORMAL, text="Перевести ➔")
qt_win.after(0, update_ui)
threading.Thread(target=task, daemon=True).start()
if event:
return "break"
btn_trans = ttk.Button(bot_f, text="Перевести ➔", command=run_translation)
btn_trans.pack(side=tk.LEFT)
# Биндинги клавиш для поля ввода
txt_in.bind("<Return>", run_translation)
txt_in.bind("<Shift-Return>", lambda e: None)
if __name__ == "__main__":
root = tk.Tk()
app = TranslatorApp(root)
root.mainloop()
+420
View File
@@ -0,0 +1,420 @@
import os
import re
import tokenize
import io
import time
import random
import threading
import textwrap
import shutil
import customtkinter as ctk
from tkinter import filedialog, messagebox
from deep_translator import GoogleTranslator
from deep_translator.exceptions import RequestError
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
class TranslationWorker(threading.Thread):
def __init__(self, path, is_file, recursive, extensions, target_lang, only_cjk, save_as_new, create_bak, log_callback, progress_callback, finish_callback):
super().__init__()
self.path = path
self.is_file = is_file
self.recursive = recursive
self.extensions =[ext.strip().lower() for ext in extensions.split(',')]
self.target_lang = target_lang
self.only_cjk = only_cjk
self.save_as_new = save_as_new
self.create_bak = create_bak
self.log = log_callback
self.update_progress = progress_callback
self.finish = finish_callback
self.translator = GoogleTranslator(source='auto', target=self.target_lang)
self.request_count = 0
self.is_running = True
def run(self):
self.log("Начало сканирования...")
files_to_process =[]
# Защита от зацикливания: игнорируем файлы, которые уже являются переведенными копиями
suffix = f"_{self.target_lang}"
if self.is_file:
files_to_process.append(self.path)
else:
if self.recursive:
for root, _, files in os.walk(self.path):
for file in files:
if any(file.lower().endswith(ext) for ext in self.extensions):
# Пропускаем уже переведенные файлы
name, ext = os.path.splitext(file)
if not name.endswith(suffix):
files_to_process.append(os.path.join(root, file))
else:
for file in os.listdir(self.path):
if os.path.isfile(os.path.join(self.path, file)):
if any(file.lower().endswith(ext) for ext in self.extensions):
name, ext = os.path.splitext(file)
if not name.endswith(suffix):
files_to_process.append(os.path.join(self.path, file))
total_files = len(files_to_process)
self.log(f"Найдено файлов для обработки: {total_files}")
if total_files == 0:
self.finish()
return
for idx, filepath in enumerate(files_to_process):
if not self.is_running:
self.log("Процесс остановлен пользователем.")
break
self.log(f"Обработка: {os.path.basename(filepath)}")
try:
if filepath.endswith('.py'):
self.process_python_file(filepath)
elif filepath.endswith(('.qml', '.js', '.json', '.cpp', '.cs')):
self.process_regex_file(filepath)
else:
self.process_plain_text(filepath)
except Exception as e:
self.log(f"Ошибка в файле {os.path.basename(filepath)}: {str(e)}")
self.update_progress((idx + 1) / total_files)
self.log("Перевод завершен!")
self.finish()
def stop(self):
self.is_running = False
def check_limits(self):
self.request_count += 1
time.sleep(random.uniform(0.5, 1.5))
if self.request_count % 50 == 0:
self.log("Пауза 5 сек во избежание блокировки Google API...")
time.sleep(random.uniform(5.0, 8.0))
def has_target_chars(self, text):
if not self.only_cjk:
return re.search(r'[a-zA-Zа-яА-Я\u4e00-\u9fff\u3040-\u30ff]', text) is not None
return bool(re.search(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf]', text))
def fix_orthography(self, text):
if not text: return text
text = re.sub(r'%\s+([a-zA-Z0-9])', r'%\1', text)
text = re.sub(r'\{\s*(.*?)\s*\}', r'{\1}', text)
text = re.sub(r'\\ \s*n', r'\\n', text)
text = re.sub(r'\\ \s*t', r'\\t', text)
text = text.replace('...', '')
return text
def translate_text(self, text):
if not text.strip() or not self.has_target_chars(text):
return text
try:
if len(text) > 4000:
chunks = textwrap.wrap(text, 4000, break_long_words=False, replace_whitespace=False)
translated_chunks =[]
for chunk in chunks:
self.check_limits()
res = self.translator.translate(chunk)
translated_chunks.append(res)
translated = " ".join(translated_chunks)
else:
self.check_limits()
translated = self.translator.translate(text)
return self.fix_orthography(translated)
except RequestError:
self.log("Ошибка сети. Повторная попытка через 3 сек...")
time.sleep(3)
return self.translate_text(text)
except Exception as e:
self.log(f"Ошибка перевода: {e}")
return text
def get_save_path(self, filepath):
if self.save_as_new:
base, ext = os.path.splitext(filepath)
return f"{base}_{self.target_lang}{ext}"
return filepath
def handle_backup(self, filepath):
if self.create_bak and not self.save_as_new:
shutil.copy2(filepath, filepath + ".bak")
def process_python_file(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)
replacements =[]
for tok in tokens:
if tok.type in (tokenize.STRING, tokenize.COMMENT) and self.has_target_chars(tok.string):
if tok.type == tokenize.COMMENT:
m = re.match(r'^(#\s*)(.*)', tok.string, re.DOTALL)
if m:
prefix, content = m.groups()
replacements.append((tok.start, tok.end, prefix + self.translate_text(content)))
elif tok.type == tokenize.STRING:
m = re.match(r'^([a-zA-Z]*)("{1,3}|\'{1,3})(.*)(\2)$', tok.string, re.DOTALL)
if m:
prefix, quote, content, _ = m.groups()
translated = self.translate_text(content)
if 'r' not in prefix.lower() and len(quote) == 1:
translated = translated.replace(quote, f"\\{quote}")
replacements.append((tok.start, tok.end, f"{prefix}{quote}{translated}{quote}"))
if replacements:
self.handle_backup(filepath)
self._apply_replacements(filepath, self.get_save_path(filepath), source, replacements)
def process_regex_file(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
pattern = re.compile(
r'(/\*.*?\*/)|(//.*?$)|("(?:\\.|[^"\\])*")|(\'(?:\\.|[^\'\\])*\')|(`(?:\\.|[^`\\])*`)',
re.DOTALL | re.MULTILINE
)
replacements =[]
for match in pattern.finditer(source):
text = match.group(0)
if self.has_target_chars(text):
if text.startswith('/*'):
replacements.append((match.start(), match.end(), f"/*{self.translate_text(text[2:-2])}*/"))
elif text.startswith('//'):
replacements.append((match.start(), match.end(), f"//{self.translate_text(text[2:])}"))
else:
quote = text[0]
content = text[1:-1]
translated = self.translate_text(content).replace(quote, f"\\{quote}")
replacements.append((match.start(), match.end(), f"{quote}{translated}{quote}"))
if replacements:
self.handle_backup(filepath)
for start, end, new_text in reversed(replacements):
source = source[:start] + new_text + source[end:]
with open(self.get_save_path(filepath), 'w', encoding='utf-8') as f:
f.write(source)
def process_plain_text(self, filepath):
with open(filepath, 'r', encoding='utf-8') as f:
source = f.read()
if self.has_target_chars(source):
self.handle_backup(filepath)
translated = self.translate_text(source)
with open(self.get_save_path(filepath), 'w', encoding='utf-8') as f:
f.write(translated)
def _apply_replacements(self, original_filepath, save_filepath, source, replacements):
lines = source.splitlines(keepends=True)
for start, end, new_text in reversed(replacements):
start_row, start_col = start[0] - 1, start[1]
end_row, end_col = end[0] - 1, end[1]
if start_row == end_row:
lines[start_row] = lines[start_row][:start_col] + new_text + lines[start_row][end_col:]
else:
lines[start_row] = lines[start_row][:start_col] + new_text
for r in range(start_row + 1, end_row):
lines[r] = ""
lines[end_row] = lines[end_row][end_col:]
with open(save_filepath, 'w', encoding='utf-8') as f:
f.write("".join(lines))
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Code & Text Translator Pro")
self.geometry("900x650")
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=2)
self.grid_rowconfigure(0, weight=1)
self.worker = None
self.selected_path = ""
self.is_file_mode = False
self._build_ui()
def _build_ui(self):
# Левая панель (Настройки)
self.frame_settings = ctk.CTkScrollableFrame(self)
self.frame_settings.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
ctk.CTkLabel(self.frame_settings, text="Настройки", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(0, 10))
# Кнопки выбора
frame_btns = ctk.CTkFrame(self.frame_settings, fg_color="transparent")
frame_btns.pack(fill="x", pady=5, padx=5)
self.btn_folder = ctk.CTkButton(frame_btns, text="Выбрать папку", command=self.select_folder)
self.btn_folder.pack(side="left", fill="x", expand=True, padx=(0, 2))
self.btn_file = ctk.CTkButton(frame_btns, text="Выбрать файл", command=self.select_file)
self.btn_file.pack(side="right", fill="x", expand=True, padx=(2, 0))
self.lbl_path = ctk.CTkLabel(self.frame_settings, text="Ничего не выбрано", text_color="gray", wraplength=250)
self.lbl_path.pack(pady=5, padx=10)
# Режимы сохранения
ctk.CTkLabel(self.frame_settings, text="Безопасность:", font=ctk.CTkFont(weight="bold")).pack(pady=(15, 0), anchor="w", padx=10)
self.var_new_file = ctk.BooleanVar(value=True)
self.chk_new_file = ctk.CTkCheckBox(self.frame_settings, text="Создать новый файл (суффикс _ru)", variable=self.var_new_file, command=self.toggle_backup_chk)
self.chk_new_file.pack(pady=5, padx=10, anchor="w")
self.var_bak = ctk.BooleanVar(value=False)
self.chk_bak = ctk.CTkCheckBox(self.frame_settings, text="Создавать бэкап (.bak)", variable=self.var_bak, state="disabled")
self.chk_bak.pack(pady=5, padx=10, anchor="w")
# Чекбокс подпапок
self.var_recursive = ctk.BooleanVar(value=True)
self.chk_recursive = ctk.CTkCheckBox(self.frame_settings, text="Включая подпапки", variable=self.var_recursive)
self.chk_recursive.pack(pady=(15, 5), padx=10, anchor="w")
# Расширения
ctk.CTkLabel(self.frame_settings, text="Расширения файлов (через запятую):").pack(pady=(10, 0), padx=10, anchor="w")
self.ent_exts = ctk.CTkEntry(self.frame_settings, placeholder_text=".py, .qml, .txt")
self.ent_exts.insert(0, ".py, .qml, .txt, .json")
self.ent_exts.pack(pady=5, padx=10, fill="x")
# Язык
ctk.CTkLabel(self.frame_settings, text="Целевой язык:").pack(pady=(10, 0), padx=10, anchor="w")
self.opt_lang = ctk.CTkOptionMenu(self.frame_settings, values=["ru (Русский)", "en (Английский)", "uk (Украинский)"])
self.opt_lang.pack(pady=5, padx=10, fill="x")
# Ограничение по CJK
self.var_cjk = ctk.BooleanVar(value=True)
self.chk_cjk = ctk.CTkCheckBox(self.frame_settings, text="Только Китайский/Японский", variable=self.var_cjk)
self.chk_cjk.pack(pady=10, padx=10, anchor="w")
# Пространство, чтобы кнопки всегда были внизу
ctk.CTkLabel(self.frame_settings, text="").pack(expand=True, fill="both")
# Кнопки управления
self.btn_start = ctk.CTkButton(self.frame_settings, text="▶ НАЧАТЬ ПЕРЕВОД", fg_color="green", hover_color="darkgreen", command=self.start_translation)
self.btn_start.pack(pady=(15, 5), padx=10, fill="x")
self.btn_stop = ctk.CTkButton(self.frame_settings, text="ОСТАНОВИТЬ", fg_color="red", hover_color="darkred", state="disabled", command=self.stop_translation)
self.btn_stop.pack(pady=5, padx=10, fill="x")
# Правая панель (Логи и Прогресс)
self.frame_logs = ctk.CTkFrame(self)
self.frame_logs.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
self.frame_logs.grid_rowconfigure(1, weight=1)
self.frame_logs.grid_columnconfigure(0, weight=1)
ctk.CTkLabel(self.frame_logs, text="Журнал работы:", font=ctk.CTkFont(size=14, weight="bold")).grid(row=0, column=0, pady=5, padx=10, sticky="w")
self.txt_log = ctk.CTkTextbox(self.frame_logs, state="disabled", wrap="word", font=ctk.CTkFont(family="Consolas", size=12))
self.txt_log.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
self.progress_bar = ctk.CTkProgressBar(self.frame_logs)
self.progress_bar.grid(row=2, column=0, padx=10, pady=15, sticky="ew")
self.progress_bar.set(0)
def toggle_backup_chk(self):
"""Если включено создание нового файла, бэкап не нужен."""
if self.var_new_file.get():
self.var_bak.set(False)
self.chk_bak.configure(state="disabled")
else:
self.chk_bak.configure(state="normal")
def select_folder(self):
folder = filedialog.askdirectory()
if folder:
self.lbl_path.configure(text=folder)
self.selected_path = folder
self.is_file_mode = False
self.chk_recursive.configure(state="normal")
def select_file(self):
file = filedialog.askopenfilename(filetypes=[("All Supported Files", "*.*")])
if file:
self.lbl_path.configure(text=file)
self.selected_path = file
self.is_file_mode = True
self.chk_recursive.configure(state="disabled")
def log(self, message):
def update():
self.txt_log.configure(state="normal")
self.txt_log.insert("end", message + "\n")
self.txt_log.see("end")
self.txt_log.configure(state="disabled")
self.after(0, update)
def update_progress(self, value):
self.after(0, lambda: self.progress_bar.set(value))
def start_translation(self):
if not self.selected_path:
messagebox.showwarning("Внимание", "Сначала выберите файл или папку!")
return
target_lang = self.opt_lang.get().split(" ")[0]
extensions = self.ent_exts.get()
if not extensions and not self.is_file_mode:
messagebox.showwarning("Внимание", "Укажите хотя бы одно расширение!")
return
# Блокировка UI
self.btn_start.configure(state="disabled")
self.btn_folder.configure(state="disabled")
self.btn_file.configure(state="disabled")
self.chk_new_file.configure(state="disabled")
self.chk_bak.configure(state="disabled")
self.btn_stop.configure(state="normal")
self.progress_bar.set(0)
self.txt_log.configure(state="normal")
self.txt_log.delete("1.0", "end")
self.txt_log.configure(state="disabled")
self.worker = TranslationWorker(
path=self.selected_path,
is_file=self.is_file_mode,
recursive=self.var_recursive.get(),
extensions=extensions,
target_lang=target_lang,
only_cjk=self.var_cjk.get(),
save_as_new=self.var_new_file.get(),
create_bak=self.var_bak.get(),
log_callback=self.log,
progress_callback=self.update_progress,
finish_callback=self.translation_finished
)
self.worker.start()
def stop_translation(self):
if self.worker:
self.worker.stop()
self.log("Остановка процессов... Пожалуйста, подождите завершения текущего файла.")
self.btn_stop.configure(state="disabled")
def translation_finished(self):
def update():
self.btn_start.configure(state="normal")
self.btn_folder.configure(state="normal")
self.btn_file.configure(state="normal")
self.chk_new_file.configure(state="normal")
self.toggle_backup_chk() # Восстанавливаем состояние чекбокса бэкапов
self.btn_stop.configure(state="disabled")
self.after(0, update)
if __name__ == "__main__":
app = App()
app.mainloop()