Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
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.
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
Vendored
BIN
Binary file not shown.
@@ -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()
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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` (выдавать предупреждение "Неизвестный издатель"). Это абсолютно нормальная реакция системы на программы без платной цифровой подписи разработчика. Просто нажмите "Подробнее" -> "Выполнить в любом случае" или добавьте файл в исключения.
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user