├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── fonts ├── JetBrainsMono-Regular.ttf ├── JetBrainsMono-SemiBold.ttf ├── LICENSE-JetBrains.txt ├── LICENSE-Roboto.txt └── RobotoRegular.ttf ├── main.py ├── models ├── __init__.py ├── abstact.py ├── dictionary.py └── main.py ├── packer ├── __init__.py ├── dbpf.py ├── resource.py └── stbl.py ├── prefs ├── dlc.ini ├── interface │ ├── brasil.xml │ ├── chinese.xml │ ├── danish.xml │ ├── english.xml │ ├── french.xml │ ├── german.xml │ ├── indonesian.xml │ ├── italian.xml │ ├── polish.xml │ ├── russian.xml │ ├── spanish.xml │ └── ukrainian.xml └── languages.xml ├── requirements.txt ├── resource_rc.py ├── resources ├── images │ ├── api.png │ ├── close.png │ ├── copy.png │ ├── dark │ │ ├── arrow_down.png │ │ ├── arrow_up.png │ │ ├── backspace.png │ │ ├── checkbox_checked.png │ │ ├── checkbox_checked_disabled.png │ │ ├── checkbox_unchecked.png │ │ ├── checkbox_unchecked_disabled.png │ │ ├── checked.png │ │ ├── checked_hover.png │ │ ├── menu_right_arrow.png │ │ ├── menu_right_arrow_disabled.png │ │ ├── menu_right_arrow_hover.png │ │ ├── radio_checked.png │ │ ├── radio_checked_disabled.png │ │ ├── radio_unchecked.png │ │ └── radio_unchecked_disabled.png │ ├── dict.png │ ├── edit.png │ ├── export.png │ ├── export_xml.png │ ├── import.png │ ├── lang.png │ ├── light │ │ ├── arrow_down.png │ │ ├── arrow_up.png │ │ ├── backspace.png │ │ ├── checkbox_checked.png │ │ ├── checkbox_checked_disabled.png │ │ ├── checkbox_unchecked.png │ │ ├── checkbox_unchecked_disabled.png │ │ ├── checked.png │ │ ├── checked_hover.png │ │ ├── menu_right_arrow.png │ │ ├── menu_right_arrow_disabled.png │ │ ├── menu_right_arrow_hover.png │ │ ├── radio_checked.png │ │ ├── radio_checked_disabled.png │ │ ├── radio_unchecked.png │ │ └── radio_unchecked_disabled.png │ ├── load.png │ ├── logo.png │ ├── options.png │ ├── paste.png │ ├── replace.png │ ├── search_dest.png │ ├── search_id.png │ ├── search_source.png │ ├── translate.png │ ├── undo.png │ ├── validate_0.png │ ├── validate_1.png │ ├── validate_2.png │ ├── validate_3.png │ └── validate_4.png ├── logo.ico ├── resource.qrc └── theme.qss ├── singletons ├── __init__.py ├── config.py ├── expansions.py ├── interface.py ├── languages.py ├── signals.py ├── state.py ├── translator.py └── undo.py ├── storages ├── __init__.py ├── container.py ├── dictionaries.py ├── packages.py └── records.py ├── themes ├── __init__.py ├── dark.py ├── light.py └── stylesheet.py ├── utils ├── __init__.py ├── constants.py └── functions.py ├── widgets ├── __init__.py ├── colorbar.py ├── delegate.py ├── editor.py ├── lineedit.py ├── tableview.py └── toolbar.py └── windows ├── __init__.py ├── edit_dialog.py ├── export_dialog.py ├── import_dialog.py ├── main_window.py ├── options_dialog.py ├── replace_dialog.py ├── translate_dialog.py └── ui ├── __init__.py ├── edit_dialog.py ├── export_dialog.py ├── import_dialog.py ├── main_window.py ├── options_dialog.py ├── replace_dialog.py └── translate_dialog.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | 12 | # IDE 13 | .vscode/ 14 | .idea/ 15 | 16 | build/ 17 | dist/ 18 | dictionary/ 19 | prefs/config.xml 20 | prefs/interface/sync.py 21 | setup.py 22 | 23 | *.bat 24 | *.dct 25 | *.svg 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.4 - 2025-08-05 4 | 5 | - Resolved issues with Google Translate (It's not perfect, so check the results) 6 | - Added support for MyMemory translator as an alternative to Google Translate 7 | - Added support for translating multiple text selections simultaneously 8 | - Added support for JSON and binary files used by Sims 4 Studio 9 | - Added Danish language 10 | - Added French language 11 | - Added Indonesian language 12 | - Added Ukrainian language 13 | 14 | Many changes are taken from other forks, for which special thanks to their authors (TURBODRIVER, EliD-Dev, etc). 15 | 16 | ## Version 1.3.1 - 2024-08-02 17 | 18 | - Added German language 19 | - Fixed working with merged packages 20 | - Variables in curly brackets are no longer translated using translators (such as {0.String}, {0.Number}, etc) 21 | 22 | ## Version 1.3 - 2024-07-09 23 | 24 | - Added light and dark themes 25 | - Added Polish language 26 | 27 | ## Version 1.2 - 2024-06-18 28 | 29 | - Added a visual indicator of translation progress 30 | - Fixed several bugs 31 | 32 | ## Version 1.2 RC - 2024-06-15 33 | 34 | - Added the possibility of automatic translation 35 | - Added DeepL support 36 | - The translation files of this application are placed in separate files 37 | - The Sims 4 language settings are placed in a separate file 38 | 39 | ## Version 1.1.2 - 2020-12-07 40 | 41 | - Fixed translation using Google Translate 42 | - Fixed a bug related to incorrect line selection when editing (Windows 7) 43 | 44 | ## Version 1.1 - 2020-11-18 45 | 46 | - The list of extensions is made in a separate file, so that I don't have to upload a new version every time, and you download it (for those who use the dictionaries of the base game and extensions) 47 | - Fixed crash with a large number of dictionaries 48 | 49 | ## Version 1.0.1 - 2020-09-24 50 | 51 | - Fixed bug with access to settings 52 | 53 | ## Version 1.0 - 2020-09-24 54 | 55 | - Added Chinese language 56 | - Аdded 64-bit version (32-bit version will no longer be supported) 57 | - A lot of fixes that are a bit lazy to list 58 | 59 | ## Version 1.0 RC - 2020-08-24 60 | 61 | - Added support for STBL files 62 | - Added support for exporting XML files for the Deaderpool's STBL editor 63 | - Added the ability to insert new record 64 | - Added the ability to set the high-bit for groups 65 | - Added the ability to translate from dictionaries created for other mods 66 | - Improved export 67 | - Fixed an error when starting the program if there are non-Latin characters in the path 68 | 69 | ## Version 0.4 - 2020-07-10 70 | 71 | - Added Google Translate 72 | - Trying to get rid of virus warnings in Avast antivirus 73 | 74 | ## Version 0.3 - 2020-07-04 75 | 76 | - Added support for import XML files created with Deaderpool's STBL editor 77 | - Attempt to fix some problems in the interface 78 | 79 | ## Version 0.2 - 2020-07-02 80 | 81 | - Added support for simplified Chinese (CHS_CN) 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Sims 4 Translator 2 | 3 | _I don't know much English, so I used a translator to describe it. I'm sorry about that!_ 4 | 5 | This is a simple program for easy translation of mods for The Sims 4. 6 | 7 | ## Features 8 | 9 | - The translation of mods can be saved in dictionaries, which saves you from re-translating if new versions are released. 10 | - Saving the translation as a separate file, or adding it directly to the mod. 11 | - Saving translations from multiple mods to a single file. 12 | 13 | ## Usage Instructions 14 | 15 | - In the settings, select the languages you plan to translate from and to. 16 | - Open the mod (*.package) 17 | - Translate. 18 | - Save the dictionary if you plan to edit the translation or are going to translate new versions of the mod. 19 | - Save the translation in a separate package or save the translation immediately in an open mod (finalization). 20 | - Profit. 21 | 22 | ## Languages 23 | 24 | Starting from version 1.2, the translation of the interface has been made into separate files. They are available in the folder: 25 | 26 | `/prefs/interface/` 27 | 28 | To add a new translation, you need to place the XML file with the new translation in this folder. 29 | 30 | ## Fonts 31 | 32 | This project uses the following fonts: 33 | - **[Roboto](https://fonts.google.com/specimen/Roboto)** — Licensed under [Apache 2.0](fonts/LICENSE-Roboto.txt). 34 | - **[JetBrains Mono](https://www.jetbrains.com/lp/mono/)** — Licensed under [SIL Open Font License 1.1 (OFL)](fonts/LICENSE-JetBrains.txt). 35 | 36 | ## Additional Credits 37 | 38 | The idea is taken from the app [xTranslator](https://www.nexusmods.com/skyrimspecialedition/mods/134). -------------------------------------------------------------------------------- /fonts/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/fonts/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /fonts/JetBrainsMono-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/fonts/JetBrainsMono-SemiBold.ttf -------------------------------------------------------------------------------- /fonts/LICENSE-JetBrains.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /fonts/LICENSE-Roboto.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /fonts/RobotoRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/fonts/RobotoRegular.ttf -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import glob 6 | from PySide6.QtWidgets import QApplication 7 | from PySide6.QtGui import QFontDatabase 8 | 9 | from windows.main_window import MainWindow 10 | 11 | from singletons.state import app_state 12 | 13 | from storages.packages import PackagesStorage 14 | from storages.dictionaries import DictionariesStorage 15 | 16 | from themes.stylesheet import stylesheet 17 | 18 | import resource_rc 19 | 20 | 21 | def main(): 22 | sys.argv += ['-style', 'windows'] 23 | 24 | app = QApplication(sys.argv) 25 | 26 | packages_storage = PackagesStorage() 27 | dictionaries_storage = DictionariesStorage() 28 | 29 | app_state.set_packages_storage(packages_storage) 30 | app_state.set_dictionaries_storage(dictionaries_storage) 31 | 32 | for path in glob.glob('fonts/*.ttf'): 33 | QFontDatabase.addApplicationFont(os.path.abspath(path)) 34 | 35 | app.setStyleSheet(stylesheet()) 36 | 37 | window = MainWindow() 38 | window.show() 39 | 40 | exit_status = app.exec() 41 | 42 | app.setStyleSheet('') 43 | 44 | sys.exit(exit_status) 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/models/__init__.py -------------------------------------------------------------------------------- /models/abstact.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QAbstractTableModel 4 | from PySide6.QtGui import QColor 5 | 6 | import themes.dark as dark 7 | 8 | from singletons.config import config 9 | 10 | 11 | class AbstractTableModel(QAbstractTableModel): 12 | 13 | def __init__(self, parent=None): 14 | super().__init__(parent) 15 | 16 | self.items = [] 17 | self.filtered = [] 18 | 19 | self.addition_sort = -1 20 | 21 | is_dark_theme = config.value('interface', 'theme') == 'dark' 22 | 23 | self.color_null = QColor(dark.TEXT_DISABLED) if is_dark_theme else QColor(114, 114, 213) 24 | 25 | def rowCount(self, parent=None): 26 | return len(self.filtered) 27 | 28 | def columnCount(self, parent=None): 29 | return 0 30 | 31 | def append(self, rows): 32 | self.beginResetModel() 33 | if rows: 34 | if isinstance(rows[0], list): 35 | self.items.extend(rows) 36 | else: 37 | self.items.append(rows) 38 | self.endResetModel() 39 | 40 | def replace(self, rows): 41 | self.beginResetModel() 42 | self.items.clear() 43 | self.filtered.clear() 44 | self.items = rows 45 | self.endResetModel() 46 | 47 | def filter(self, rows): 48 | self.beginResetModel() 49 | self.filtered.clear() 50 | self.filtered = rows 51 | self.endResetModel() 52 | 53 | def clear(self): 54 | self.beginResetModel() 55 | for item in self.items: 56 | item.clear() 57 | self.items.clear() 58 | self.filtered.clear() 59 | self.endResetModel() 60 | -------------------------------------------------------------------------------- /models/dictionary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import operator 4 | from PySide6.QtCore import Qt, QSortFilterProxyModel 5 | 6 | from .abstact import AbstractTableModel 7 | 8 | from singletons.interface import interface 9 | from singletons.state import app_state 10 | from utils.functions import text_to_table 11 | from utils.constants import * 12 | 13 | 14 | class Model(AbstractTableModel): 15 | 16 | def __init__(self, parent=None): 17 | super().__init__(parent) 18 | 19 | self.addition_sort = RECORD_DICTIONARY_PACKAGE 20 | 21 | self.index_mapping = { 22 | COLUMN_DICTIONARIES_PACKAGE: RECORD_DICTIONARY_PACKAGE, 23 | COLUMN_DICTIONARIES_SOURCE: RECORD_DICTIONARY_SOURCE, 24 | COLUMN_DICTIONARIES_TRANSLATE: RECORD_DICTIONARY_TRANSLATE, 25 | } 26 | 27 | def columnCount(self, parent=None): 28 | return 5 29 | 30 | def data(self, index, role=Qt.ItemDataRole.DisplayRole): 31 | if not index.isValid(): 32 | return None 33 | 34 | row = index.row() 35 | column = index.column() 36 | 37 | if row < 0 or row >= len(self.filtered): 38 | return None 39 | 40 | item = self.filtered[row] 41 | 42 | if role == Qt.ItemDataRole.FontRole: 43 | if column == COLUMN_DICTIONARIES_PACKAGE: 44 | return app_state.monospace.font() 45 | 46 | elif role == Qt.ItemDataRole.ForegroundRole: 47 | if column in (COLUMN_DICTIONARIES_SOURCE, COLUMN_DICTIONARIES_TRANSLATE): 48 | if column == COLUMN_DICTIONARIES_SOURCE: 49 | txt = item[RECORD_DICTIONARY_SOURCE] 50 | else: 51 | txt = item[RECORD_DICTIONARY_TRANSLATE] 52 | if not txt or not str(txt).strip(' '): 53 | return self.color_null 54 | 55 | elif role == Qt.ItemDataRole.DisplayRole: 56 | if not column: 57 | return None 58 | 59 | if column == COLUMN_DICTIONARIES_PACKAGE: 60 | return item[RECORD_DICTIONARY_PACKAGE] 61 | 62 | elif column in (COLUMN_DICTIONARIES_SOURCE, COLUMN_DICTIONARIES_TRANSLATE): 63 | if column == COLUMN_DICTIONARIES_SOURCE: 64 | txt = item[RECORD_DICTIONARY_SOURCE] 65 | else: 66 | txt = item[RECORD_DICTIONARY_TRANSLATE] 67 | if txt: 68 | if not str(txt).strip(' '): 69 | return '[SPACEBAR]' * (len(txt.split(' ')) - 1) 70 | else: 71 | return text_to_table(txt) 72 | return '[NULL]' 73 | 74 | return None 75 | 76 | def sort(self, column, order=Qt.SortOrder.AscendingOrder): 77 | self.beginResetModel() 78 | reverse = order == Qt.SortOrder.DescendingOrder 79 | idx = self.index_mapping.get(column, RECORD_DICTIONARY_LENGTH) 80 | key = operator.itemgetter(idx, 81 | self.addition_sort) if 0 <= self.addition_sort != idx else operator.itemgetter(idx) 82 | self.filtered.sort(key=key, reverse=reverse) 83 | self.endResetModel() 84 | 85 | 86 | class ProxyModel(QSortFilterProxyModel): 87 | 88 | def __init__(self, parent=None): 89 | super().__init__(parent) 90 | 91 | self.__text = None 92 | 93 | self.__column = COLUMN_DICTIONARIES_LENGTH 94 | self.__order = Qt.SortOrder.AscendingOrder 95 | 96 | def filter(self, text: str): 97 | self.__text = text.lower() if text else None 98 | self.process_filter() 99 | 100 | def process_filter(self): 101 | model = self.sourceModel() 102 | if self.__text: 103 | filtered_data = [i for i in model.items if self.__text in str(i[RECORD_DICTIONARY_SOURCE]).lower()] 104 | model.filter(filtered_data) 105 | model.sort(self.__column, self.__order) 106 | else: 107 | model.filter([]) 108 | 109 | def headerData(self, section, orientation, role=None): 110 | if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: 111 | header_mapping = { 112 | COLUMN_DICTIONARIES_PACKAGE: 'ID', 113 | COLUMN_DICTIONARIES_SOURCE: interface.text('MainTableView', 'Original'), 114 | COLUMN_DICTIONARIES_TRANSLATE: interface.text('MainTableView', 'Translated') 115 | } 116 | return header_mapping.get(section, '') 117 | return None 118 | 119 | def sort(self, column, order=Qt.SortOrder.AscendingOrder): 120 | self.__column = column 121 | self.__order = order 122 | self.sourceModel().sort(column, order) 123 | -------------------------------------------------------------------------------- /models/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import operator 4 | from PySide6.QtCore import Qt, QSortFilterProxyModel 5 | from typing import Union, List 6 | 7 | from .abstact import AbstractTableModel 8 | 9 | from singletons.config import config 10 | from singletons.interface import interface 11 | from singletons.state import app_state 12 | from utils.functions import text_to_table 13 | from utils.constants import * 14 | 15 | 16 | class Model(AbstractTableModel): 17 | 18 | def __init__(self, parent=None): 19 | super().__init__(parent) 20 | 21 | self.addition_sort = RECORD_MAIN_INDEX 22 | 23 | self.index_mapping = { 24 | COLUMN_MAIN_ID: RECORD_MAIN_ID, 25 | COLUMN_MAIN_INSTANCE: RECORD_MAIN_INSTANCE, 26 | COLUMN_MAIN_GROUP: RECORD_MAIN_GROUP, 27 | COLUMN_MAIN_SOURCE: RECORD_MAIN_SOURCE, 28 | COLUMN_MAIN_TRANSLATE: RECORD_MAIN_TRANSLATE, 29 | COLUMN_MAIN_FLAG: RECORD_MAIN_FLAG, 30 | COLUMN_MAIN_COMMENT: RECORD_MAIN_COMMENT 31 | } 32 | 33 | def columnCount(self, parent=None): 34 | return 9 35 | 36 | def data(self, index, role=Qt.ItemDataRole.DisplayRole): 37 | if not index.isValid(): 38 | return None 39 | 40 | row = index.row() 41 | column = index.column() 42 | 43 | if row < 0 or row >= len(self.filtered): 44 | return None 45 | 46 | item = self.filtered[row] 47 | 48 | if role == Qt.ItemDataRole.FontRole: 49 | if column in (COLUMN_MAIN_INDEX, COLUMN_MAIN_ID, COLUMN_MAIN_GROUP, COLUMN_MAIN_INSTANCE): 50 | return app_state.monospace.font() 51 | 52 | elif role == Qt.ItemDataRole.ForegroundRole: 53 | if column in (COLUMN_MAIN_SOURCE, COLUMN_MAIN_TRANSLATE): 54 | txt = item.source if column == COLUMN_MAIN_SOURCE else item.translate 55 | if not txt or not str(txt).strip(' '): 56 | return self.color_null 57 | 58 | elif role == Qt.ItemDataRole.DisplayRole: 59 | if not column: 60 | return None 61 | 62 | if column == COLUMN_MAIN_INDEX: 63 | numeration = config.value('view', 'numeration') 64 | if numeration == NUMERATION_SOURCE: 65 | return item.idx_source 66 | elif numeration == NUMERATION_XML_DP: 67 | instance = app_state.current_instance 68 | return item[RECORD_MAIN_INDEX_ALT][3] if instance > 0 else item[RECORD_MAIN_INDEX_ALT][2] 69 | else: 70 | if app_state.current_package: 71 | return item[RECORD_MAIN_INDEX_ALT][0] 72 | else: 73 | return item.idx 74 | 75 | elif column == COLUMN_MAIN_ID: 76 | return item.id_hex 77 | 78 | elif column == COLUMN_MAIN_INSTANCE: 79 | return item.instance_hex 80 | 81 | elif column == COLUMN_MAIN_GROUP: 82 | return item.group_hex 83 | 84 | elif column in (COLUMN_MAIN_SOURCE, COLUMN_MAIN_TRANSLATE): 85 | txt = item.source if column == COLUMN_MAIN_SOURCE else item.translate 86 | if txt: 87 | if not str(txt).strip(' '): 88 | return '[SPACEBAR]' * (len(txt.split(' ')) - 1) 89 | else: 90 | return text_to_table(txt) 91 | return '[NULL]' 92 | 93 | elif column == COLUMN_MAIN_COMMENT: 94 | return item.comment 95 | 96 | return None 97 | 98 | def sort(self, column, order=Qt.SortOrder.AscendingOrder): 99 | self.beginResetModel() 100 | reverse = order == Qt.SortOrder.DescendingOrder 101 | idx = self.index_mapping.get(column, RECORD_MAIN_INDEX) 102 | key = operator.itemgetter(idx, 103 | self.addition_sort) if 0 <= self.addition_sort != idx else operator.itemgetter(idx) 104 | self.filtered.sort(key=key, reverse=reverse) 105 | self.endResetModel() 106 | 107 | 108 | class ProxyModel(QSortFilterProxyModel): 109 | 110 | def __init__(self, parent=None): 111 | super().__init__(parent) 112 | 113 | self.__package = None 114 | self.__instance = None 115 | self.__text = None 116 | self.__mode = SEARCH_IN_SOURCE 117 | self.__flags = [] 118 | self.__different = False 119 | 120 | self.__column = COLUMN_MAIN_INDEX 121 | self.__order = Qt.SortOrder.AscendingOrder 122 | 123 | def filter(self, package: str, instance: Union[str, int], text: str, mode: int, flags: List[int], different: bool): 124 | self.__package = package 125 | self.__mode = mode 126 | self.__flags = flags if flags else [] 127 | self.__different = different 128 | 129 | if text: 130 | if mode == SEARCH_IN_ID: 131 | try: 132 | self.__text = int(text, 16) 133 | except ValueError: 134 | self.__text = -1 135 | else: 136 | self.__text = text.lower() 137 | else: 138 | self.__text = None 139 | 140 | if instance: 141 | try: 142 | self.__instance = int(instance, 16) if isinstance(instance, str) else instance 143 | except ValueError: 144 | self.__instance = -1 145 | else: 146 | self.__instance = None 147 | 148 | self.process_filter() 149 | 150 | def process_filter(self): 151 | model = self.sourceModel() 152 | filtered_data = [i for i in model.items if self.check_filter(i)] 153 | model.filter(filtered_data) 154 | model.sort(self.__column, self.__order) 155 | 156 | def check_filter(self, item): 157 | if (self.__package and item[RECORD_MAIN_PACKAGE] != self.__package) \ 158 | or (self.__instance and item[RECORD_MAIN_INSTANCE] != self.__instance) \ 159 | or (self.__flags and item[RECORD_MAIN_FLAG] in self.__flags): 160 | return False 161 | 162 | if self.__different: 163 | record_main_translate_old = item[RECORD_MAIN_TRANSLATE_OLD] 164 | record_main_source_old = item[RECORD_MAIN_SOURCE_OLD] 165 | if not record_main_translate_old and not record_main_source_old: 166 | return False 167 | 168 | if self.__text: 169 | if self.__mode == SEARCH_IN_ID: 170 | return self.__text == item[RECORD_MAIN_ID] 171 | elif self.__mode == SEARCH_IN_SOURCE: 172 | return self.__text in item[RECORD_MAIN_SOURCE].lower() 173 | elif self.__mode == SEARCH_IN_DESTINATION: 174 | return self.__text in item[RECORD_MAIN_TRANSLATE].lower() 175 | else: 176 | return False 177 | 178 | return True 179 | 180 | def headerData(self, section, orientation, role=None): 181 | if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: 182 | header_mapping = { 183 | COLUMN_MAIN_INDEX: '#', 184 | COLUMN_MAIN_ID: interface.text('MainTableView', 'ID'), 185 | COLUMN_MAIN_INSTANCE: interface.text('MainTableView', 'Instance'), 186 | COLUMN_MAIN_GROUP: interface.text('MainTableView', 'Group'), 187 | COLUMN_MAIN_SOURCE: interface.text('MainTableView', 'Original'), 188 | COLUMN_MAIN_TRANSLATE: interface.text('MainTableView', 'Translated'), 189 | COLUMN_MAIN_COMMENT: interface.text('MainTableView', 'Comment') 190 | } 191 | return header_mapping.get(section, '') 192 | return None 193 | 194 | def sort(self, column, order=Qt.SortOrder.AscendingOrder): 195 | self.__column = column 196 | self.__order = order 197 | self.sourceModel().sort(column, order) 198 | -------------------------------------------------------------------------------- /packer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import io 4 | import contextlib 5 | import struct 6 | import zlib 7 | import json 8 | 9 | 10 | class Packer: 11 | 12 | def __init__(self, bstr, mode='r'): 13 | self.writable = (mode == 'w') 14 | 15 | if isinstance(bstr, bytes): 16 | self.raw_len = len(bstr) 17 | bstr = io.BytesIO(bstr) 18 | else: 19 | bstr.seek(0, io.SEEK_END) 20 | self.raw_len = bstr.tell() 21 | bstr.seek(0) 22 | 23 | self.raw = bstr 24 | 25 | @property 26 | def seek(self): 27 | return self.raw.tell() 28 | 29 | @seek.setter 30 | def seek(self, val): 31 | if self.writable or val <= self.raw_len: 32 | self.raw.seek(val) 33 | 34 | @contextlib.contextmanager 35 | def at(self, pos): 36 | saved = self.seek 37 | try: 38 | if pos is not None: 39 | self.seek = pos 40 | yield 41 | finally: 42 | self.seek = saved 43 | 44 | def _get_int(self, size, signed=False): 45 | return int.from_bytes(self.raw.read(size), 'little', signed=signed) 46 | 47 | def get_byte(self): 48 | return self._get_int(1, False) 49 | 50 | def get_float(self): 51 | return struct.unpack('f', self.get_raw_bytes(4))[0] 52 | 53 | def get_int8(self): 54 | return self._get_int(1, True) 55 | 56 | def get_int16(self): 57 | return self._get_int(2, True) 58 | 59 | def get_int32(self): 60 | return self._get_int(4, True) 61 | 62 | def get_int64(self): 63 | return self._get_int(8, True) 64 | 65 | def get_uint8(self): 66 | return self._get_int(1, False) 67 | 68 | def get_uint16(self): 69 | return self._get_int(2, False) 70 | 71 | def get_uint32(self): 72 | return self._get_int(4, False) 73 | 74 | def get_uint64(self): 75 | return self._get_int(8, False) 76 | 77 | def _put_int(self, i, length, signed): 78 | self.raw.write(i.to_bytes(length, 'little', signed=signed)) 79 | 80 | def put_byte(self, i): 81 | self._put_int(i, 1, False) 82 | 83 | def put_float(self, i): 84 | self.put_raw_bytes(struct.pack('f', i)) 85 | 86 | def put_int8(self, i): 87 | self._put_int(i, 1, True) 88 | 89 | def put_int16(self, i): 90 | self._put_int(i, 2, True) 91 | 92 | def put_int32(self, i): 93 | self._put_int(i, 4, True) 94 | 95 | def put_int64(self, i): 96 | self._put_int(i, 8, True) 97 | 98 | def put_uint8(self, i): 99 | self._put_int(i, 1, False) 100 | 101 | def put_uint16(self, i): 102 | self._put_int(i, 2, False) 103 | 104 | def put_uint32(self, i): 105 | self._put_int(i, 4, False) 106 | 107 | def put_uint64(self, i): 108 | self._put_int(i, 8, False) 109 | 110 | def get_raw_bytes(self, count): 111 | return self.raw.read(count) 112 | 113 | def put_raw_bytes(self, bstr): 114 | self.raw.write(bstr) 115 | 116 | def get_string(self, length=0, compress=True): 117 | if not length: 118 | length = self.get_uint32() 119 | compress = self.get_byte() 120 | content = self.get_raw_bytes(length) 121 | if compress: 122 | content = zlib.decompress(content) 123 | return content.decode('utf-8') 124 | 125 | def put_string(self, value): 126 | value = value.encode('utf-8') 127 | if len(value) > 50: 128 | content = zlib.compress(value) 129 | self.put_uint32(len(content)) 130 | self.put_byte(1) 131 | self.put_raw_bytes(content) 132 | else: 133 | self.put_uint32(len(value)) 134 | self.put_byte(0) 135 | self.put_raw_bytes(value) 136 | 137 | def get_json(self, length=0): 138 | return json.loads(self.get_string(length)) 139 | 140 | def put_json(self, value): 141 | self.put_string(json.dumps(value)) 142 | 143 | def get_content(self): 144 | with self.at(0): 145 | return self.raw.read() 146 | 147 | def close(self): 148 | self.raw.close() 149 | -------------------------------------------------------------------------------- /packer/resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from collections import namedtuple 5 | from typing import Union 6 | 7 | from singletons.config import config 8 | from singletons.languages import languages 9 | 10 | 11 | FORMATTERS = { 12 | 's4pe': 'S4_{id.type:08X}_{id.group:08X}_{id.instance:016X}', 13 | 'colon': '{id.group:08x}:{id.instance:016x}:{id.type:08x}', 14 | 'maxis': '{id.group:08x}!{id.instance:016x}.{id.type:08x}', 15 | } 16 | 17 | PARSERS = { 18 | 's4pe': '^S4_{type}_{group}_{instance}(?:%%.*)?$', 19 | 'colon': '^{group}:{instance}:{type}$', 20 | 'maxis': '^{group}!{instance}.{type}$', 21 | } 22 | 23 | for fmt, value in PARSERS.items(): 24 | PARSERS[fmt] = re.compile(value.format(type="(?P[0-9A-Fa-f]{,8})", 25 | group="(?P[0-9A-Fa-f]{,8})", 26 | instance="(?P[0-9A-Fa-f]{,16})")) 27 | 28 | 29 | class Resource(namedtuple('Resource', 'id locator size package')): 30 | 31 | __slots__ = () 32 | 33 | @property 34 | def content(self): 35 | return self.package.content(self) 36 | 37 | def __eq__(self, other): 38 | return (self.id == other.id and 39 | self.locator == other.locator and 40 | self.size == other.size and 41 | self.package is other.package) 42 | 43 | def __ne__(self, other): 44 | return not (self == other) 45 | 46 | 47 | class ResourceID(namedtuple('ResourceID', 'group instance type')): 48 | 49 | __slots__ = () 50 | 51 | DEFAULT_FMT = 's4pe' 52 | 53 | def __str__(self) -> str: 54 | return FORMATTERS[self.DEFAULT_FMT].format(id=self) 55 | 56 | @property 57 | def filename(self) -> str: 58 | return str(self) 59 | 60 | @classmethod 61 | def from_string(cls, string: str): 62 | for parser in PARSERS.values(): 63 | m = parser.match(string) 64 | if m: 65 | return cls( 66 | int(m.group('group'), 16), 67 | int(m.group('instance'), 16), 68 | int(m.group('type'), 16), 69 | ) 70 | else: 71 | return cls(group=0x80000000 if config.value('group', 'highbit') else 0, 72 | instance=0, 73 | type=0x220557DA) 74 | 75 | @property 76 | def str_group(self) -> str: 77 | return '{group:08x}'.format(group=self.group) 78 | 79 | @property 80 | def str_instance(self) -> str: 81 | return '{instance:016x}'.format(instance=self.instance) 82 | 83 | @property 84 | def hex_instance(self) -> str: 85 | return '0x{instance:016X}'.format(instance=self.instance) 86 | 87 | @property 88 | def base_instance(self) -> int: 89 | hex_inst = self.hex_instance 90 | return int(hex_inst[4:], 16) 91 | 92 | @property 93 | def is_stbl(self) -> bool: 94 | return self.type == 0x220557DA 95 | 96 | @property 97 | def language(self) -> Union[str, None]: 98 | if self.type == 0x220557DA: 99 | code = '0x{id:016X}'.format(id=self.instance)[:4] 100 | language = languages.by_code(code) 101 | return language.locale if language else None 102 | return None 103 | 104 | @property 105 | def language_code(self) -> Union[str, None]: 106 | if self.type == 0x220557DA: 107 | return '0x{id:016X}'.format(id=self.instance)[:4] 108 | return None 109 | 110 | def convert_group(self, highbit: bool = False): 111 | group = ('8' if highbit else '0') + self.str_group[1:] 112 | return self._replace(group=int(group, 16)) 113 | 114 | def convert_instance(self, locale: str = None): 115 | if not locale: 116 | locale = config.value('translation', 'destination') 117 | language = languages.by_locale(locale) 118 | instance = int(language.code + self.str_instance[2:], 16) 119 | return self._replace(instance=instance) 120 | -------------------------------------------------------------------------------- /packer/stbl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from packer import Packer 4 | 5 | 6 | class Stbl: 7 | 8 | def __init__(self, rid, value=None): 9 | self.rid = rid 10 | self.value = value 11 | 12 | self._strings = {} 13 | 14 | @property 15 | def language(self): 16 | return self.rid.language 17 | 18 | @property 19 | def strings(self): 20 | if self.value is None: 21 | return {} 22 | 23 | f = Packer(self.value, mode='r') 24 | 25 | if f.get_raw_bytes(4) != b'STBL': 26 | return {} 27 | 28 | version = f.get_uint16() 29 | if version != 5: 30 | return {} 31 | 32 | _compressed = f.get_uint8() 33 | num_entries = f.get_uint64() 34 | 35 | f.seek += 2 36 | 37 | _strings_length = f.get_uint32() 38 | 39 | _strings = {} 40 | 41 | get_uint32 = f.get_uint32 42 | 43 | for i in range(num_entries): 44 | key = get_uint32() 45 | _flags = f.get_uint8() 46 | length = f.get_uint16() 47 | val = f.get_raw_bytes(length).decode('utf-8') 48 | _strings[key] = val 49 | 50 | return _strings 51 | 52 | @property 53 | def binary(self): 54 | f = Packer(b'', mode='w') 55 | 56 | f.put_raw_bytes(b'STBL') 57 | f.put_uint16(5) 58 | f.put_uint8(0) 59 | 60 | num_entries = len(self._strings) 61 | f.put_uint64(num_entries) 62 | 63 | f.seek += 2 64 | 65 | strings_length = num_entries 66 | for key, value in self._strings.items(): 67 | strings_length += len(value.encode('utf-8')) 68 | 69 | f.put_uint32(strings_length) 70 | 71 | for key, value in self._strings.items(): 72 | f.put_uint32(key) 73 | f.put_int8(0) 74 | _value = value.encode('utf-8') 75 | f.put_uint16(len(_value)) 76 | f.put_raw_bytes(_value) 77 | 78 | return f.get_content() 79 | 80 | def add(self, key, value): 81 | self._strings[key] = value.replace("\r", '').replace("\n", '\\n') if value else '' 82 | -------------------------------------------------------------------------------- /prefs/languages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyperclip 2 | PySide6 3 | requests 4 | -------------------------------------------------------------------------------- /resources/images/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/api.png -------------------------------------------------------------------------------- /resources/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/close.png -------------------------------------------------------------------------------- /resources/images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/copy.png -------------------------------------------------------------------------------- /resources/images/dark/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/arrow_down.png -------------------------------------------------------------------------------- /resources/images/dark/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/arrow_up.png -------------------------------------------------------------------------------- /resources/images/dark/backspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/backspace.png -------------------------------------------------------------------------------- /resources/images/dark/checkbox_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checkbox_checked.png -------------------------------------------------------------------------------- /resources/images/dark/checkbox_checked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checkbox_checked_disabled.png -------------------------------------------------------------------------------- /resources/images/dark/checkbox_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checkbox_unchecked.png -------------------------------------------------------------------------------- /resources/images/dark/checkbox_unchecked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checkbox_unchecked_disabled.png -------------------------------------------------------------------------------- /resources/images/dark/checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checked.png -------------------------------------------------------------------------------- /resources/images/dark/checked_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/checked_hover.png -------------------------------------------------------------------------------- /resources/images/dark/menu_right_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/menu_right_arrow.png -------------------------------------------------------------------------------- /resources/images/dark/menu_right_arrow_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/menu_right_arrow_disabled.png -------------------------------------------------------------------------------- /resources/images/dark/menu_right_arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/menu_right_arrow_hover.png -------------------------------------------------------------------------------- /resources/images/dark/radio_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/radio_checked.png -------------------------------------------------------------------------------- /resources/images/dark/radio_checked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/radio_checked_disabled.png -------------------------------------------------------------------------------- /resources/images/dark/radio_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/radio_unchecked.png -------------------------------------------------------------------------------- /resources/images/dark/radio_unchecked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dark/radio_unchecked_disabled.png -------------------------------------------------------------------------------- /resources/images/dict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/dict.png -------------------------------------------------------------------------------- /resources/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/edit.png -------------------------------------------------------------------------------- /resources/images/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/export.png -------------------------------------------------------------------------------- /resources/images/export_xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/export_xml.png -------------------------------------------------------------------------------- /resources/images/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/import.png -------------------------------------------------------------------------------- /resources/images/lang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/lang.png -------------------------------------------------------------------------------- /resources/images/light/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/arrow_down.png -------------------------------------------------------------------------------- /resources/images/light/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/arrow_up.png -------------------------------------------------------------------------------- /resources/images/light/backspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/backspace.png -------------------------------------------------------------------------------- /resources/images/light/checkbox_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checkbox_checked.png -------------------------------------------------------------------------------- /resources/images/light/checkbox_checked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checkbox_checked_disabled.png -------------------------------------------------------------------------------- /resources/images/light/checkbox_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checkbox_unchecked.png -------------------------------------------------------------------------------- /resources/images/light/checkbox_unchecked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checkbox_unchecked_disabled.png -------------------------------------------------------------------------------- /resources/images/light/checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checked.png -------------------------------------------------------------------------------- /resources/images/light/checked_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/checked_hover.png -------------------------------------------------------------------------------- /resources/images/light/menu_right_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/menu_right_arrow.png -------------------------------------------------------------------------------- /resources/images/light/menu_right_arrow_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/menu_right_arrow_disabled.png -------------------------------------------------------------------------------- /resources/images/light/menu_right_arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/menu_right_arrow_hover.png -------------------------------------------------------------------------------- /resources/images/light/radio_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/radio_checked.png -------------------------------------------------------------------------------- /resources/images/light/radio_checked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/radio_checked_disabled.png -------------------------------------------------------------------------------- /resources/images/light/radio_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/radio_unchecked.png -------------------------------------------------------------------------------- /resources/images/light/radio_unchecked_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/light/radio_unchecked_disabled.png -------------------------------------------------------------------------------- /resources/images/load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/load.png -------------------------------------------------------------------------------- /resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/logo.png -------------------------------------------------------------------------------- /resources/images/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/options.png -------------------------------------------------------------------------------- /resources/images/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/paste.png -------------------------------------------------------------------------------- /resources/images/replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/replace.png -------------------------------------------------------------------------------- /resources/images/search_dest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/search_dest.png -------------------------------------------------------------------------------- /resources/images/search_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/search_id.png -------------------------------------------------------------------------------- /resources/images/search_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/search_source.png -------------------------------------------------------------------------------- /resources/images/translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/translate.png -------------------------------------------------------------------------------- /resources/images/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/undo.png -------------------------------------------------------------------------------- /resources/images/validate_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/validate_0.png -------------------------------------------------------------------------------- /resources/images/validate_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/validate_1.png -------------------------------------------------------------------------------- /resources/images/validate_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/validate_2.png -------------------------------------------------------------------------------- /resources/images/validate_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/validate_3.png -------------------------------------------------------------------------------- /resources/images/validate_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/images/validate_4.png -------------------------------------------------------------------------------- /resources/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/resources/logo.ico -------------------------------------------------------------------------------- /resources/resource.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo.ico 4 | 5 | images/translate.png 6 | images/export_xml.png 7 | images/api.png 8 | images/search_dest.png 9 | images/search_id.png 10 | images/search_source.png 11 | images/close.png 12 | images/copy.png 13 | images/dict.png 14 | images/edit.png 15 | images/export.png 16 | images/import.png 17 | images/lang.png 18 | images/load.png 19 | images/options.png 20 | images/paste.png 21 | images/replace.png 22 | images/undo.png 23 | images/validate_0.png 24 | images/validate_1.png 25 | images/validate_2.png 26 | images/validate_3.png 27 | images/validate_4.png 28 | 29 | theme.qss 30 | 31 | images/light/arrow_down.png 32 | images/light/arrow_up.png 33 | images/light/backspace.png 34 | images/light/checkbox_checked.png 35 | images/light/checkbox_checked_disabled.png 36 | images/light/checkbox_unchecked.png 37 | images/light/checkbox_unchecked_disabled.png 38 | images/light/checked.png 39 | images/light/checked_hover.png 40 | images/light/menu_right_arrow.png 41 | images/light/menu_right_arrow_hover.png 42 | images/light/menu_right_arrow_disabled.png 43 | images/light/radio_checked.png 44 | images/light/radio_checked_disabled.png 45 | images/light/radio_unchecked.png 46 | images/light/radio_unchecked_disabled.png 47 | 48 | images/dark/arrow_down.png 49 | images/dark/arrow_up.png 50 | images/dark/backspace.png 51 | images/dark/checkbox_checked.png 52 | images/dark/checkbox_checked_disabled.png 53 | images/dark/checkbox_unchecked.png 54 | images/dark/checkbox_unchecked_disabled.png 55 | images/dark/checked.png 56 | images/dark/checked_hover.png 57 | images/dark/menu_right_arrow.png 58 | images/dark/menu_right_arrow_hover.png 59 | images/dark/menu_right_arrow_disabled.png 60 | images/dark/radio_checked.png 61 | images/dark/radio_checked_disabled.png 62 | images/dark/radio_unchecked.png 63 | images/dark/radio_unchecked_disabled.png 64 | 65 | 66 | -------------------------------------------------------------------------------- /singletons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/singletons/__init__.py -------------------------------------------------------------------------------- /singletons/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import ctypes 5 | import ctypes.wintypes 6 | import xml.etree.ElementTree as ElementTree 7 | from xml.dom import minidom 8 | from typing import Union 9 | 10 | from utils.constants import * 11 | 12 | 13 | def is_dark_theme(): 14 | registry = ctypes.windll.advapi32 15 | hkey_current_user = 0x80000001 16 | sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize' 17 | value_name = 'AppsUseLightTheme' 18 | 19 | hkey = ctypes.wintypes.HKEY() 20 | result = registry.RegOpenKeyExW(hkey_current_user, sub_key, 0, 0x20019, ctypes.byref(hkey)) 21 | 22 | if result != 0: 23 | return False 24 | 25 | value = ctypes.wintypes.DWORD() 26 | value_length = ctypes.wintypes.DWORD(ctypes.sizeof(value)) 27 | result = registry.RegQueryValueExW(hkey, value_name, 0, None, ctypes.byref(value), ctypes.byref(value_length)) 28 | registry.RegCloseKey(hkey) 29 | 30 | if result != 0: 31 | return False 32 | 33 | return value.value == 0 34 | 35 | 36 | class ConfigManager: 37 | 38 | DEFAULTS = { 39 | 'interface': { 40 | 'language': 'en_US', 41 | 'theme': '' 42 | }, 43 | 'dictionaries': { 44 | 'gamepath': '', 45 | 'dictpath': '', 46 | 'strong': True 47 | }, 48 | 'save': { 49 | 'backup': True, 50 | 'experemental': False 51 | }, 52 | 'group': { 53 | 'original': True, 54 | 'highbit': False, 55 | 'lowbit': False 56 | }, 57 | 'template': { 58 | 'conflict': '1_{name}_{lang_d}', 59 | 'non_conflict': 'z_{name}_{lang_d}' 60 | }, 61 | 'translation': { 62 | 'source': 'ENG_US', 63 | 'destination': 'RUS_RU' 64 | }, 65 | 'api': { 66 | 'engine': '', 67 | 'deepl_key': '' 68 | }, 69 | 'view': { 70 | 'id': True, 71 | 'instance': False, 72 | 'group': False, 73 | 'source': True, 74 | 'comment': False, 75 | 'colorbar': True, 76 | 'numeration': NUMERATION_STANDART 77 | }, 78 | 'temporary': { 79 | 'directory': os.path.abspath(os.path.expanduser('~/Documents')) 80 | } 81 | } 82 | 83 | def __init__(self) -> None: 84 | self.__config_file = './prefs/config.xml' 85 | self.__config = self.DEFAULTS.copy() 86 | self.__load() 87 | 88 | def __load(self) -> None: 89 | try: 90 | self.__update_defaults_from_file() 91 | except (ElementTree.ParseError, FileNotFoundError) as e: 92 | self.save() 93 | 94 | def save(self) -> None: 95 | root = ElementTree.Element('config') 96 | for section, options in self.__config.items(): 97 | section_element = ElementTree.SubElement(root, section) 98 | for option, value in options.items(): 99 | option_element = ElementTree.SubElement(section_element, option) 100 | option_element.text = self.__convert_to_str(value) 101 | 102 | rough = ElementTree.tostring(root, encoding='utf-8').decode('utf-8') 103 | reparsed = minidom.parseString(rough) 104 | prettyxml = reparsed.toprettyxml(indent=' ', encoding='utf-8') 105 | 106 | with open(self.__config_file, 'wb') as fp: 107 | fp.write(prettyxml) 108 | 109 | def value(self, section: str, option: str) -> Union[str, int, bool, None]: 110 | return self.__config.get(section, {}).get(option) 111 | 112 | def set_value(self, section: str, option: str, value: Union[str, int, bool]) -> None: 113 | if section not in self.__config: 114 | self.__config[section] = {} 115 | self.__config[section][option] = value 116 | 117 | def __update_defaults_from_file(self) -> None: 118 | tree = ElementTree.parse(self.__config_file) 119 | root = tree.getroot() 120 | for section in root: 121 | section_name = section.tag 122 | for option in section: 123 | option_name = option.tag 124 | option_value = self.__convert_value(option.text) 125 | if section_name not in self.__config: 126 | self.__config[section_name] = {} 127 | self.__config[section_name][option_name] = option_value 128 | 129 | @staticmethod 130 | def __convert_value(value: str) -> Union[str, int, bool, None]: 131 | if value is None: 132 | return '' 133 | if value.lower() in ['true', 'false']: 134 | return value.lower() == 'true' 135 | try: 136 | return int(value) 137 | except ValueError: 138 | return value 139 | 140 | @staticmethod 141 | def __convert_to_str(value: Union[str, int, bool]) -> str: 142 | if isinstance(value, bool): 143 | return 'true' if value else 'false' 144 | return str(value) 145 | 146 | @property 147 | def theme_name(self): 148 | name = self.value('interface', 'theme') 149 | if name: 150 | return name 151 | name = 'dark' if is_dark_theme() else 'light' 152 | self.set_value('interface', 'theme', name) 153 | return name 154 | 155 | def is_dark_theme(self): 156 | return self.theme_name == 'dark' 157 | 158 | 159 | config = ConfigManager() 160 | -------------------------------------------------------------------------------- /singletons/expansions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from collections import namedtuple 5 | from typing import List, Dict, Union 6 | 7 | from singletons.config import config 8 | from singletons.interface import interface 9 | 10 | 11 | class Expansion(namedtuple('Expansion', 'names folder')): 12 | 13 | names: Union[str, Dict[str, str]] 14 | folder: str 15 | 16 | @property 17 | def status(self) -> str: 18 | if self.exists: 19 | if not self.exists_source: 20 | return interface.text('OptionsDialog', '{} not exist').format(expansions.strings_source) 21 | elif not self.exists_dest: 22 | return interface.text('OptionsDialog', '{} not exist').format(expansions.strings_dest) 23 | else: 24 | return interface.text('OptionsDialog', 'FOUND') 25 | else: 26 | return interface.text('OptionsDialog', 'NOT FOUND') 27 | 28 | @property 29 | def name(self) -> str: 30 | if isinstance(self.names, str): 31 | return interface.text('OptionsDialog', self.names) 32 | elif isinstance(self.names, dict): 33 | key = 'name_' + config.value('interface', 'language').lower() 34 | return self.names.get(key, self.names.get('name_en_us', self.folder)) 35 | else: 36 | return self.folder 37 | 38 | @property 39 | def offset(self) -> str: 40 | return '' if '/' in self.folder else ' ' 41 | 42 | @property 43 | def dictionary(self) -> str: 44 | return 'BASE' if '/' in self.folder else self.folder 45 | 46 | @property 47 | def file_source(self) -> str: 48 | return str( 49 | os.path.join(config.value('dictionaries', 'gamepath'), 50 | self.folder, expansions.strings_source + '.package')) 51 | 52 | @property 53 | def file_dest(self) -> str: 54 | return str(os.path.join(config.value('dictionaries', 'gamepath'), 55 | self.folder, expansions.strings_dest + '.package')) 56 | 57 | @property 58 | def exists_source(self) -> bool: 59 | return os.path.exists(self.file_source) 60 | 61 | @property 62 | def exists_dest(self) -> bool: 63 | return os.path.exists(self.file_dest) 64 | 65 | @property 66 | def exists_strings(self) -> bool: 67 | return self.exists_source and self.exists_dest 68 | 69 | @property 70 | def exists(self) -> bool: 71 | path = config.value('dictionaries', 'gamepath') 72 | if path: 73 | return os.path.exists(os.path.join(path, self.folder)) 74 | return False 75 | 76 | 77 | class Expansions: 78 | 79 | def __init__(self) -> None: 80 | self.__packs = None 81 | 82 | @property 83 | def items(self) -> List[Union[str, Expansion]]: 84 | baseexp = Expansion('BASE GAME', 'Data/Client') 85 | 86 | rows = [baseexp] 87 | 88 | exp = ['', 'Expansion packs'] 89 | game = ['', 'Game packs'] 90 | stuff = ['', 'Stuff packs'] 91 | 92 | packs = self._parse_expansion_packs() 93 | 94 | if packs: 95 | for key, items in packs.items(): 96 | if key.upper().startswith('EP'): 97 | exp.append(Expansion(items, key)) 98 | elif key.upper().startswith('GP'): 99 | game.append(Expansion(items, key)) 100 | elif key.upper().startswith('SP'): 101 | stuff.append(Expansion(items, key)) 102 | 103 | elif baseexp.exists_source: 104 | for dirname in os.listdir(config.value('dictionaries', 'gamepath')): 105 | if dirname.upper().startswith('EP'): 106 | exp.append(Expansion(dirname, dirname)) 107 | elif dirname.upper().startswith('GP'): 108 | game.append(Expansion(dirname, dirname)) 109 | elif dirname.upper().startswith('SP'): 110 | stuff.append(Expansion(dirname, dirname)) 111 | 112 | if len(exp) > 2: 113 | rows.extend(exp) 114 | if len(game) > 2: 115 | rows.extend(game) 116 | if len(stuff) > 2: 117 | rows.extend(stuff) 118 | 119 | return rows 120 | 121 | @property 122 | def strings_source(self) -> str: 123 | return 'Strings_' + config.value('translation', 'source') 124 | 125 | @property 126 | def strings_dest(self) -> str: 127 | return 'Strings_' + config.value('translation', 'destination') 128 | 129 | def exists(self) -> List[Expansion]: 130 | return [exp for exp in self.items if isinstance(exp, Expansion) and exp.exists_strings] 131 | 132 | def _parse_expansion_packs(self) -> Dict[str, Dict[str, str]]: 133 | if self.__packs is not None: 134 | return self.__packs 135 | 136 | self.__packs = {} 137 | 138 | try: 139 | with open('./prefs/dlc.ini', 'r', encoding='utf-8') as fp: 140 | content = fp.read() 141 | except FileNotFoundError: 142 | return {} 143 | 144 | current_pack = None 145 | 146 | for line in content.split('\n'): 147 | line = line.strip() 148 | if line.startswith('[') and line.endswith(']'): 149 | pack_code = line[1:-1] 150 | current_pack = {} 151 | self.__packs[pack_code] = current_pack 152 | elif '=' in line and current_pack is not None: 153 | key, value = line.split('=', 1) 154 | key = key.lower().strip() 155 | if key.startswith('name_'): 156 | current_pack[key] = value.strip() 157 | 158 | return self.__packs 159 | 160 | 161 | expansions = Expansions() 162 | -------------------------------------------------------------------------------- /singletons/interface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import glob 5 | import xml.etree.ElementTree as ElementTree 6 | from collections import namedtuple 7 | from typing import List 8 | 9 | from singletons.config import config 10 | 11 | 12 | class Lang(namedtuple('Lang', 'code name items authors version')): 13 | 14 | def get(self, k: str, v: str) -> str: 15 | if k in self.items and v in self.items[k]: 16 | value = self.items[k][v] 17 | return value.strip() if value is not None else v.strip() 18 | return v.strip() 19 | 20 | 21 | class Interface: 22 | 23 | def __init__(self) -> None: 24 | self.__languages = {} 25 | self.__current = None 26 | self.__load() 27 | 28 | def __load(self) -> None: 29 | files = glob.glob(os.path.join('./prefs/interface', '*.xml')) 30 | 31 | for f in files: 32 | with open(f, 'r', encoding='utf-8') as fp: 33 | content = fp.read() 34 | 35 | try: 36 | parser = ElementTree.XMLParser(encoding='utf-8') 37 | root = ElementTree.fromstring(content, parser=parser) 38 | except ElementTree.ParseError: 39 | continue 40 | 41 | code = root.get('language') 42 | name = root.get('name') 43 | authors = root.get('authors') 44 | version = root.get('version') 45 | 46 | if code and name: 47 | lang_items = {} 48 | 49 | for context in root.findall('context'): 50 | key = context.get('name') 51 | if key: 52 | context_items = {} 53 | 54 | for s in context.findall('string'): 55 | source = s.find('source') 56 | translation = s.find('translation') 57 | if source is not None and translation is not None: 58 | context_items[source.text] = translation.text 59 | 60 | lang_items[key] = context_items 61 | 62 | self.__languages[code] = Lang(code, name, lang_items, authors, version) 63 | 64 | self.__languages = dict(sorted(self.__languages.items())) 65 | 66 | self.reload() 67 | 68 | def reload(self) -> None: 69 | self.__current = self.__languages.get(config.value('interface', 'language'), None) 70 | 71 | def text(self, k: str, v: str) -> str: 72 | return self.__current.get(k, v) if isinstance(self.__current, Lang) else v 73 | 74 | @property 75 | def authors(self) -> str: 76 | return self.__current.authors if isinstance(self.__current, Lang) else None 77 | 78 | @property 79 | def version(self) -> str: 80 | return self.__current.version if isinstance(self.__current, Lang) else None 81 | 82 | @property 83 | def languages(self) -> List[Lang]: 84 | return list(self.__languages.values()) 85 | 86 | @property 87 | def current_index(self) -> int: 88 | keys = list(self.__languages.keys()) 89 | interface_value = config.value('interface', 'language') 90 | if interface_value in keys: 91 | return keys.index(interface_value) 92 | return 0 93 | 94 | 95 | interface = Interface() 96 | -------------------------------------------------------------------------------- /singletons/languages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from collections import namedtuple 5 | import xml.etree.ElementTree as ElementTree 6 | 7 | from singletons.config import config 8 | 9 | 10 | Language = namedtuple('Language', 'locale code google deepl') 11 | 12 | 13 | class Languages: 14 | 15 | def __init__(self) -> None: 16 | self.__locales = {} 17 | self.__codes = {} 18 | self.__load() 19 | 20 | def __load(self): 21 | languages_file = os.path.abspath('./prefs/languages.xml') 22 | 23 | if not os.path.exists(languages_file): 24 | return 25 | 26 | with open(languages_file, 'r', encoding='utf-8') as fp: 27 | content = fp.read() 28 | 29 | try: 30 | parser = ElementTree.XMLParser(encoding='utf-8') 31 | root = ElementTree.fromstring(content, parser=parser) 32 | except ElementTree.ParseError: 33 | return 34 | 35 | for item in root.findall('language'): 36 | locale = item.get('locale') 37 | code = item.get('code') 38 | if locale and code: 39 | lang = Language(locale.upper(), code, item.get('google-code'), item.get('deepl-code')) 40 | self.__locales[lang.locale] = lang 41 | self.__codes[lang.code] = lang 42 | 43 | @property 44 | def locales(self) -> list: 45 | return list(self.__locales.keys()) 46 | 47 | @property 48 | def source(self) -> Language: 49 | return self.by_locale(config.value('translation', 'source')) 50 | 51 | @property 52 | def destination(self) -> Language: 53 | return self.by_locale(config.value('translation', 'destination')) 54 | 55 | def by_locale(self, locale: str) -> Language: 56 | return self.__locales.get(locale) 57 | 58 | def by_code(self, code: str) -> Language: 59 | return self.__codes.get(code) 60 | 61 | 62 | languages = Languages() 63 | -------------------------------------------------------------------------------- /singletons/signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QObject, Signal 4 | 5 | 6 | class ProgressSignals(QObject): 7 | initiate = Signal(str, int) 8 | increment = Signal() 9 | finished = Signal() 10 | 11 | 12 | class WindowSignals(QObject): 13 | message = Signal(str) 14 | 15 | 16 | class ColorSignals(QObject): 17 | update = Signal() 18 | 19 | 20 | class StorageSignals(QObject): 21 | updated = Signal() 22 | 23 | 24 | progress_signals = ProgressSignals() 25 | window_signals = WindowSignals() 26 | color_signals = ColorSignals() 27 | storage_signals = StorageSignals() 28 | -------------------------------------------------------------------------------- /singletons/state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | class AppState: 4 | 5 | def __init__(self): 6 | self.packages_storage = None 7 | self.dictionaries_storage = None 8 | 9 | self.current_package = None 10 | self.current_instance = 0 11 | 12 | self.tableview = None 13 | self.monospace = None 14 | 15 | def set_monospace(self, monospace): 16 | self.monospace = monospace 17 | 18 | def set_tableview(self, tableview): 19 | self.tableview = tableview 20 | 21 | def set_packages_storage(self, packages_storage): 22 | self.packages_storage = packages_storage 23 | 24 | def set_dictionaries_storage(self, dictionaries_storage): 25 | self.dictionaries_storage = dictionaries_storage 26 | 27 | def set_current_package(self, current_package): 28 | self.current_package = current_package 29 | 30 | def set_current_instance(self, current_instance): 31 | self.current_instance = current_instance 32 | 33 | 34 | app_state = AppState() 35 | -------------------------------------------------------------------------------- /singletons/translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import requests 5 | import html.parser 6 | from collections import namedtuple 7 | from typing import List 8 | 9 | from singletons.config import config 10 | from singletons.interface import interface 11 | from singletons.languages import languages 12 | 13 | 14 | Response = namedtuple('Response', 'status_code text') 15 | 16 | 17 | class Translator: 18 | 19 | @property 20 | def engines(self) -> List[str]: 21 | engines = ['Google', 'MyMemory'] 22 | if config.value('api', 'deepl_key'): 23 | engines.append('DeepL') 24 | return engines 25 | 26 | @property 27 | def available(self) -> bool: 28 | return len(self.engines) > 0 29 | 30 | @staticmethod 31 | def extract_placeholders(text): 32 | extracted_items = [] 33 | 34 | def save_and_replace_pattern(match): 35 | extracted_items.append(match.group(0)) 36 | return f"({len(extracted_items) - 1})" 37 | 38 | modified_text = re.sub(r'{[A-Za-z]?\d+\.[^{}]+}|<[^>]+>', save_and_replace_pattern, text) 39 | return modified_text, extracted_items 40 | 41 | @staticmethod 42 | def insert_placeholders(text, placeholders): 43 | for i, placeholder in enumerate(placeholders): 44 | text = text.replace(f"({i})", placeholder) 45 | 46 | text = re.sub(r'(<[^/][^>]+>)\s+', r'\1', text) 47 | text = re.sub(r'\s+(]+>)', r'\1', text) 48 | 49 | return text 50 | 51 | def translate(self, engine: str, text: str) -> Response: 52 | modified_text, placeholders = self.extract_placeholders(text) 53 | 54 | # placeholder_spaces = [] 55 | # for ph in re.finditer(r'\(\d+\)', modified_text): 56 | # before = modified_text[:ph.start()].rstrip() 57 | # after = modified_text[ph.end():].lstrip() 58 | # has_space_before = len(before) != ph.start() 59 | # has_space_after = len(after) != len(modified_text) - ph.end() 60 | # placeholder_spaces.append((has_space_before, has_space_after)) 61 | 62 | modified_text = modified_text.replace('\\n', "\n") 63 | 64 | if engine.lower() == 'mymemory': 65 | response = Translator.__mymemory(modified_text) 66 | elif engine.lower() == 'deepl': 67 | response = Translator.__deepl(modified_text) 68 | else: 69 | response = Translator.__google(modified_text) 70 | 71 | if response.status_code != 200: 72 | return response 73 | 74 | translated_text = response.text 75 | 76 | # parts = re.split(r'(\(\d+\))', translated_text) 77 | # for i in range(1, len(parts), 2): 78 | # ph_num = int(parts[i][1:-1]) 79 | # if ph_num < len(placeholder_spaces): 80 | # has_space_before, has_space_after = placeholder_spaces[ph_num] 81 | # if not has_space_before and parts[i - 1].endswith(' '): 82 | # parts[i - 1] = parts[i - 1].rstrip() 83 | # if not has_space_after and parts[i + 1].startswith(' '): 84 | # parts[i + 1] = parts[i + 1].lstrip() 85 | 86 | # translated_text = ''.join(parts) 87 | translated_text = translated_text.replace("\n", '\\n') 88 | 89 | translated_text = self.insert_placeholders(translated_text, placeholders) 90 | 91 | return Response(response.status_code, translated_text) 92 | 93 | @staticmethod 94 | def __google(text: str) -> Response: 95 | language = languages.destination 96 | 97 | if language and language.google: 98 | payload = { 99 | 'sl': 'auto', 100 | 'tl': language.google, 101 | 'q': text 102 | } 103 | 104 | url = 'http://translate.google.com/m?sl=auto' 105 | ua = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 106 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 107 | 'Chrome/122.0.0.0 Safari/537.36') 108 | 109 | try: 110 | req = requests.get(url, params=payload, headers={'User-Agent': ua}, timeout=10) 111 | if req.status_code == 200: 112 | content = req.content.decode('utf-8') 113 | expr = r'(?s)class="(?:t0|result-container)">(.*?)<' 114 | return Response(200, html.unescape(re.findall(expr, content)[0])) 115 | else: 116 | return Response(req.status_code, 117 | interface.text('Errors', 'Translation failed with error code: {}').format( 118 | req.status_code)) 119 | except Exception as e: 120 | return Response(500, str(e)) 121 | 122 | return Response(404, interface.text('Errors', 'Language code not found!')) 123 | 124 | @staticmethod 125 | def __mymemory(text: str) -> Response: 126 | if len(text) > 500: 127 | return Response(404, interface.text('Errors', 'A maximum of 500 characters is allowed.')) 128 | 129 | src = languages.source 130 | dst = languages.destination 131 | 132 | if src and src.google and dst and dst.google: 133 | payload = { 134 | 'langpair': '{}|{}'.format(src.google, dst.google), 135 | 'q': text 136 | } 137 | 138 | url = 'https://api.mymemory.translated.net/get' 139 | ua = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 140 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 141 | 'Chrome/122.0.0.0 Safari/537.36') 142 | 143 | try: 144 | req = requests.get(url, params=payload, headers={'User-Agent': ua}, timeout=10) 145 | if req.status_code == 200: 146 | content = req.json() 147 | return Response(200, content['responseData']['translatedText']) 148 | else: 149 | return Response(req.status_code, 150 | interface.text('Errors', 'Translation failed with error code: {}').format( 151 | req.status_code)) 152 | except Exception as e: 153 | return Response(500, str(e)) 154 | 155 | return Response(404, interface.text('Errors', 'Language code not found!')) 156 | 157 | @staticmethod 158 | def __deepl(text: str, xml_mode=True) -> Response: 159 | api_key = config.value('api', 'deepl_key') 160 | 161 | src = languages.source 162 | dst = languages.destination 163 | 164 | if src and src.deepl and dst and dst.deepl: 165 | payload = { 166 | 'text': text, 167 | 'source_lang': src.deepl, 168 | 'target_lang': dst.deepl, 169 | 'split_sentences': 1, 170 | 'tag_handling': 'xml' if xml_mode else 'plain' 171 | } 172 | 173 | url = 'https://api.deepl.com/v2/translate' 174 | url_free = 'https://api-free.deepl.com/v2/translate' 175 | api_url = url_free if ':fx' in api_key else url 176 | 177 | try: 178 | resp = requests.post( 179 | api_url, 180 | data=payload, 181 | headers={'Authorization': f'DeepL-Auth-Key {api_key}'}, 182 | timeout=10 183 | ) 184 | 185 | if resp.status_code == 200: 186 | txt = resp.json()['translations'][0]['text'] 187 | return Response(200, txt) 188 | elif resp.status_code == 403: 189 | return Response(403, interface.text('Errors', 'Invalid API key.')) 190 | elif resp.status_code == 456: 191 | return Response(456, interface.text('Errors', 'Your quota has exceeded!')) 192 | elif resp.status_code == 500: 193 | return Response(500, 194 | interface.text('Errors', 'There was a temporary problem with the DeepL Service.')) 195 | else: 196 | return Response(resp.status_code, 197 | interface.text('Errors', 'Translation failed with error code: {}').format( 198 | resp.status_code)) 199 | except Exception as e: 200 | return Response(500, str(e)) 201 | 202 | return Response(404, interface.text('Errors', 'Language code not found!')) 203 | 204 | 205 | translator = Translator() 206 | -------------------------------------------------------------------------------- /singletons/undo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import namedtuple 4 | from PySide6.QtCore import QObject, Signal 5 | 6 | from storages.records import MainRecord 7 | 8 | from singletons.state import app_state 9 | 10 | 11 | class UndoSignals(QObject): 12 | updated = Signal() 13 | restored = Signal() 14 | 15 | 16 | class UndoRecord(namedtuple('UndoRecord', 'items modified')): 17 | 18 | def restore(self): 19 | for item in self.items: 20 | item[0].translate = item[1] 21 | item[0].translate_old = item[2] 22 | item[0].comment = item[3] 23 | item[0].flag = item[4] 24 | 25 | 26 | class Undo: 27 | 28 | def __init__(self): 29 | self.__records = [] 30 | self.__wrapper = [] 31 | 32 | self.signals = UndoSignals() 33 | 34 | @property 35 | def available(self) -> bool: 36 | return len(self.__records) > 0 37 | 38 | def wrap(self, item: MainRecord) -> None: 39 | self.__wrapper.append((item, item.translate, item.translate_old, item.comment, item.flag)) 40 | 41 | def commit(self) -> None: 42 | if not self.__wrapper: 43 | return 44 | 45 | packages = {} 46 | for item in self.__wrapper: 47 | package_key = item[0].package 48 | packages.setdefault(package_key, []).append(item) 49 | 50 | records = {} 51 | for key, items in packages.items(): 52 | package = app_state.packages_storage.find(key) 53 | records[key] = UndoRecord(items=items, modified=package.modified) 54 | package.modify() 55 | 56 | self.__records = self.__records[-20:] + [records] 57 | 58 | self.__wrapper = [] 59 | 60 | self.signals.updated.emit() 61 | 62 | def restore(self) -> None: 63 | if self.available: 64 | for key, record in self.__records[-1].items(): 65 | package = app_state.packages_storage.find(key) 66 | if package: 67 | record.restore() 68 | package.modify(record.modified if package.modified else True) 69 | del self.__records[-1] 70 | self.signals.restored.emit() 71 | 72 | def clean(self, key: str = None) -> None: 73 | __records = [] 74 | 75 | if key: 76 | for records in self.__records: 77 | if key in records: 78 | del records[key] 79 | if records: 80 | __records.append(records) 81 | 82 | self.__records = __records 83 | 84 | self.signals.updated.emit() 85 | 86 | 87 | undo = Undo() 88 | -------------------------------------------------------------------------------- /storages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/storages/__init__.py -------------------------------------------------------------------------------- /storages/container.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | import xml.etree.ElementTree as ElementTree 6 | from json import JSONDecodeError 7 | 8 | from typing import List 9 | 10 | from packer.dbpf import DbpfPackage 11 | from packer.resource import ResourceID 12 | from packer.stbl import Stbl 13 | 14 | from singletons.config import config 15 | from singletons.state import app_state 16 | from utils.functions import md5, parsexml 17 | 18 | 19 | class Container: 20 | 21 | def __init__(self, path: str) -> None: 22 | self.path = path 23 | self.directory = os.path.dirname(path) 24 | self.fullname = os.path.basename(path) 25 | self.name = os.path.splitext(self.fullname)[0] 26 | 27 | self.key = '[' + md5(path)[0:8] + '] ' + self.fullname 28 | 29 | self.instances = [] 30 | self.modified = False 31 | 32 | self.__len = 0 33 | 34 | @property 35 | def is_package(self) -> bool: 36 | return self.fullname.lower().endswith('.package') 37 | 38 | @property 39 | def is_stbl(self) -> bool: 40 | return self.fullname.lower().endswith('.stbl') 41 | 42 | @property 43 | def is_xml(self) -> bool: 44 | return self.fullname.lower().endswith('.xml') 45 | 46 | @property 47 | def is_json(self) -> bool: 48 | return self.fullname.lower().endswith('.json') 49 | 50 | @property 51 | def is_binary(self) -> bool: 52 | return self.fullname.lower().endswith('.binary') 53 | 54 | @property 55 | def filename(self) -> str: 56 | language_source = config.value('translation', 'source') 57 | language_dest = config.value('translation', 'destination') 58 | template_conflict = config.value('template', 'conflict') 59 | template_non_conflict = config.value('template', 'non_conflict') 60 | template = template_non_conflict if config.value('save', 'experemental') else template_conflict 61 | return template.format(name=self.name, lang_s=language_source, lang_d=language_dest) 62 | 63 | def open(self) -> List[tuple]: 64 | filename = os.path.join(self.directory, self.fullname) 65 | 66 | if not os.path.exists(filename): 67 | return [] 68 | 69 | items = [] 70 | 71 | if self.is_package: 72 | items = self.open_package(filename) 73 | if self.is_stbl: 74 | items = self.open_stbl(filename) 75 | elif self.is_xml: 76 | items = self.open_xml(filename) 77 | elif self.is_json: 78 | items = self.open_json(filename) 79 | elif self.is_binary: 80 | items = self.open_binary(filename) 81 | 82 | self.__len = len(items) 83 | 84 | return items 85 | 86 | def open_package(self, filename: str) -> List[tuple]: 87 | language_source = config.value('translation', 'source') 88 | language_dest = config.value('translation', 'destination') 89 | 90 | items = [] 91 | 92 | _from = {} 93 | _to = {} 94 | _tmp = {} 95 | 96 | flag = None 97 | 98 | with DbpfPackage.read(filename) as dbfile: 99 | for rid in dbfile.search_stbl(): 100 | stbl = Stbl(rid=rid, value=dbfile[rid].content) 101 | language = rid.language 102 | if language == language_source: 103 | _from[rid] = stbl.strings 104 | elif language == language_dest: 105 | _to[rid] = stbl.strings 106 | else: 107 | if flag is None: 108 | flag = language 109 | if language == flag: 110 | _tmp[rid] = stbl.strings 111 | 112 | if not _from and not _to: 113 | if _tmp: 114 | _from = _tmp 115 | else: 116 | return [] 117 | 118 | if not _from and _to: 119 | _from = _to 120 | merge = False 121 | elif _from and not _to: 122 | merge = False 123 | else: 124 | merge = True 125 | 126 | if merge: 127 | __to = {} 128 | 129 | for rid, strings in _to.items(): 130 | for sid, dest in strings.items(): 131 | key = f'{rid.base_instance}_{sid}' 132 | __to[key] = (rid, dest) 133 | 134 | for rid, strings in _from.items(): 135 | line = 0 136 | for sid, source in strings.items(): 137 | if source: 138 | key = f'{rid.base_instance}_{sid}' 139 | dest = source 140 | if key in __to: 141 | rid = __to[key][0] 142 | dest = __to[key][1] 143 | 144 | if rid.hex_instance not in self.instances: 145 | self.instances.append(rid.hex_instance) 146 | 147 | line += 1 148 | items.append((rid, sid, source, dest, '', line, line)) 149 | 150 | else: 151 | for rid, strings in _from.items(): 152 | line = 0 153 | for sid, source in strings.items(): 154 | if source: 155 | if rid.hex_instance not in self.instances: 156 | self.instances.append(rid.hex_instance) 157 | line += 1 158 | items.append((rid, sid, source, source, '', line, line)) 159 | 160 | return items 161 | 162 | def open_stbl(self, filename: str) -> List[tuple]: 163 | items = [] 164 | 165 | rid = ResourceID.from_string(self.name) 166 | self.instances.append(rid.hex_instance) 167 | 168 | try: 169 | with open(filename, 'rb') as fp: 170 | stbl = Stbl(rid, fp.read()) 171 | line = 0 172 | for sid, source in stbl.strings.items(): 173 | line += 1 174 | items.append((rid, sid, source, source, '', line, line)) 175 | except (IOError, OSError, Exception): 176 | return [] 177 | 178 | return items 179 | 180 | def open_xml(self, filename: str) -> List[tuple]: 181 | items = [] 182 | 183 | with open(filename, 'r', encoding='utf-8') as fp: 184 | content = parsexml(fp.readlines()) 185 | 186 | try: 187 | parser = ElementTree.XMLParser(encoding='utf-8') 188 | tree = ElementTree.fromstring(content, parser=parser) 189 | except ElementTree.ParseError: 190 | return [] 191 | 192 | if tree.findall('TextStringDefinitions/TextStringDefinition'): 193 | rid = ResourceID.from_string(self.name) 194 | self.instances.append(rid.hex_instance) 195 | i = 1 196 | for s in tree.findall('TextStringDefinitions/TextStringDefinition'): 197 | sid = int(s.get('InstanceID'), 16) 198 | source = s.get('TextString') 199 | line = int(s.get('DUMMY_LINE')) 200 | items.append((rid, sid, source, source, '', line, i)) 201 | i += 1 202 | 203 | else: 204 | resources = {} 205 | 206 | for table in tree.findall('Content/Table'): 207 | instance = table.get('instance') 208 | if instance: 209 | group = table.get('group') 210 | if not group: 211 | group = '80000000' if config.value('group', 'highbit') else '00000000' 212 | 213 | key = md5(instance + group) 214 | if key not in resources: 215 | resources[key] = ResourceID(group=int(group, 16), 216 | instance=int(instance, 16), 217 | type=0x220557DA) 218 | if resources[key].hex_instance not in self.instances: 219 | self.instances.append(resources[key].hex_instance) 220 | 221 | i = 1 222 | for s in table.findall('String'): 223 | sid = int(s.get('id'), 16) 224 | source = s.find('Source').text 225 | dest = s.find('Dest').text 226 | comment = s.find('Comment').text if s.find('Comment') is not None else '' 227 | line = int(s.get('DUMMY_LINE')) 228 | items.append((resources[key], sid, source, dest, comment, line, i)) 229 | i += 1 230 | 231 | return items 232 | 233 | def open_json(self, filename: str) -> List[tuple]: 234 | items = [] 235 | 236 | try: 237 | with open(filename, 'r', encoding='utf-8') as fp: 238 | content = json.load(fp) 239 | except JSONDecodeError: 240 | return [] 241 | 242 | entries = content.get('Entries', None) 243 | 244 | if not entries: 245 | return [] 246 | 247 | rid = ResourceID.from_string(self.name) 248 | self.instances.append(rid.hex_instance) 249 | line = 0 250 | for entry in entries: 251 | sid = int(entry['Key'], 16) 252 | dest = entry['Value'] 253 | source = entry.get('Original', dest) 254 | items.append((rid, sid, source, dest, '', line, line)) 255 | line += 1 256 | 257 | return items 258 | 259 | def open_binary(self, filename: str) -> List[tuple]: 260 | items = [] 261 | 262 | rid = ResourceID.from_string(self.name) 263 | self.instances.append(rid.hex_instance) 264 | 265 | try: 266 | with open(filename, 'rb') as fp: 267 | stbl = Stbl(rid, fp.read()) 268 | line = 0 269 | for sid, source in stbl.strings.items(): 270 | line += 1 271 | items.append((rid, sid, source, source, '', line, line)) 272 | 273 | except (IOError, OSError, Exception): 274 | return [] 275 | 276 | return items 277 | 278 | def save(self, path: str = None) -> None: 279 | if not path: 280 | path = os.path.join(self.directory, self.filename + '.package') 281 | app_state.packages_storage.save(path) 282 | 283 | def finalize(self, path: str = None) -> None: 284 | if not path: 285 | path = os.path.join(self.directory, self.name + '.package') 286 | app_state.packages_storage.finalize(self.path, path) 287 | 288 | def backup(self, path: str = None) -> str: 289 | backup = (path if path else self.path) + '.backup' 290 | os.rename(self.path, backup) 291 | return backup 292 | 293 | def modify(self, state: bool = True) -> None: 294 | self.modified = state 295 | 296 | def __len__(self) -> int: 297 | return self.__len 298 | -------------------------------------------------------------------------------- /storages/dictionaries.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import zlib 5 | import json 6 | import glob 7 | from PySide6.QtCore import QObject, Signal, QThreadPool, QRunnable 8 | 9 | from packer import Packer 10 | 11 | from models.dictionary import Model, ProxyModel 12 | 13 | from singletons.config import config 14 | from singletons.interface import interface 15 | from singletons.signals import progress_signals, storage_signals 16 | from singletons.state import app_state 17 | from utils.functions import text_to_stbl 18 | from utils.constants import * 19 | 20 | 21 | class StorageSignals(QObject): 22 | updated = Signal() 23 | 24 | 25 | class UpdaterWorker(QRunnable): 26 | 27 | def __init__(self, item): 28 | super().__init__() 29 | 30 | self.item = item 31 | 32 | self.signals = StorageSignals() 33 | 34 | def run(self): 35 | source = text_to_stbl(self.item.source) 36 | translate = text_to_stbl(self.item.translate) 37 | found = self._update_or_append(source, translate) 38 | if not found: 39 | app_state.dictionaries_storage.model.append(['-', source, translate, len(source)]) 40 | storage_signals.updated.emit() 41 | 42 | @staticmethod 43 | def _update_or_append(source, translate): 44 | for model_item in app_state.dictionaries_storage.model.items: 45 | if model_item[RECORD_DICTIONARY_SOURCE] == source and model_item[RECORD_DICTIONARY_PACKAGE] == '-': 46 | model_item[RECORD_DICTIONARY_TRANSLATE] = translate 47 | return True 48 | return False 49 | 50 | 51 | class DictionariesStorage: 52 | 53 | def __init__(self) -> None: 54 | self.model = Model() 55 | self.proxy = ProxyModel() 56 | self.proxy.setSourceModel(self.model) 57 | 58 | self.directory = config.value('dictionaries', 'dictpath') 59 | if not self.directory: 60 | self.directory = os.path.abspath('./dictionary') 61 | 62 | self.loaded = False 63 | 64 | self.signals = StorageSignals() 65 | 66 | self.__sid = {} 67 | self.__sources = {} 68 | self.__hash = {} 69 | 70 | self.__pool = QThreadPool() 71 | 72 | def search(self, sid: int = None, source: str = None) -> list: 73 | if sid: 74 | return self.__sid.get(sid, []) 75 | elif source: 76 | return self.__sources.get(source, []) 77 | return [] 78 | 79 | def load(self): 80 | dictionary_files = glob.glob(os.path.join(self.directory, '*.dct')) 81 | 82 | if dictionary_files: 83 | progress_signals.initiate.emit(interface.text('System', 'Loading dictionaries...'), len(dictionary_files)) 84 | 85 | for filename in dictionary_files: 86 | dictionary_name = os.path.splitext(os.path.basename(filename))[0] 87 | 88 | with open(filename, 'rb') as fp: 89 | packer = Packer(fp.read(), mode='r') 90 | 91 | if packer.get_raw_bytes(3) == b'DCT': 92 | version = packer.get_byte() 93 | items = packer.get_json() 94 | else: 95 | content = zlib.decompress(packer.get_content()).decode('utf-8') 96 | version = 1 97 | items = json.loads(content) 98 | 99 | self.read_dictionary(dictionary_name, version, items) 100 | 101 | progress_signals.increment.emit() 102 | 103 | self.model.append(list(self.__hash.values())) 104 | self.signals.updated.emit() 105 | 106 | self.__hash.clear() 107 | 108 | progress_signals.finished.emit() 109 | 110 | self.loaded = True 111 | 112 | def read_dictionary(self, dictionary_name: str, version: int, items: list) -> None: 113 | name = dictionary_name.lower() 114 | 115 | for i, item in enumerate(items): 116 | if version == 1: 117 | item[0] = int(item[0], 16) 118 | item.append(0) 119 | 120 | if version < 3: 121 | item.append('') 122 | 123 | if version < 4: 124 | item.pop(3) 125 | 126 | if item[1] and item[1] != item[2]: 127 | self.update_hash(name, item) 128 | 129 | def update_hash(self, name: str, item: list): 130 | self.__sid.setdefault(item[0], []).append((name, item[1], item[2], item[3])) 131 | 132 | if item[1] not in self.__sources: 133 | self.__sources[item[1]] = [] 134 | if item[2] not in self.__sources[item[1]]: 135 | self.__sources[item[1]].append(item[2]) 136 | 137 | k = f'{item[1]}__{item[2]}' 138 | if k not in self.__hash: 139 | self.__hash[k] = [name, item[1], item[2], len(item[1])] 140 | 141 | def update(self, item): 142 | if not item.compare(): 143 | worker = UpdaterWorker(item) 144 | worker.setAutoDelete(True) 145 | self.__pool.start(worker) 146 | 147 | def save(self, force: bool = False, multi: bool = False): 148 | storage = app_state.packages_storage 149 | package = storage.current_package 150 | if multi or package is None: 151 | for p in storage.packages: 152 | if p.modified or force: 153 | self.save_standalone(p.name, storage.items(key=p.key)) 154 | p.modify(False) 155 | elif package is not None and (package.modified or force): 156 | self.save_standalone(package.name, storage.items(key=package.key)) 157 | package.modify(False) 158 | 159 | def save_standalone(self, name, items): 160 | if not os.path.isdir(self.directory): 161 | os.mkdir(self.directory) 162 | 163 | path = os.path.join(self.directory, name + '.dct') 164 | 165 | f = Packer(b'', mode='w') 166 | 167 | f.put_raw_bytes(b'DCT') 168 | f.put_byte(DICTIONARY_VERSION) 169 | 170 | _items = [] 171 | 172 | for item in items: 173 | if item.flag != FLAG_UNVALIDATED and item.source: 174 | _items.append([ 175 | item.id, 176 | text_to_stbl(item.source), 177 | text_to_stbl(item.translate), 178 | item.comment 179 | ]) 180 | 181 | f.put_json(_items) 182 | 183 | with open(path, 'w+b') as fp: 184 | fp.write(f.get_content()) 185 | -------------------------------------------------------------------------------- /storages/records.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from packer.resource import ResourceID 4 | from utils.functions import compare 5 | from utils.constants import * 6 | 7 | 8 | class AbstractRecord(list): 9 | 10 | def __init__(self, *args): 11 | super().__init__(args) 12 | 13 | 14 | class MainRecord(AbstractRecord): 15 | 16 | @property 17 | def idx(self) -> int: 18 | return self[RECORD_MAIN_INDEX] 19 | 20 | @idx.setter 21 | def idx(self, value: int) -> None: 22 | self[RECORD_MAIN_INDEX] = value 23 | 24 | @property 25 | def idx_standart(self) -> int: 26 | return self[RECORD_MAIN_INDEX_ALT][0] 27 | 28 | @property 29 | def idx_source(self) -> int: 30 | return self[RECORD_MAIN_INDEX_ALT][1] 31 | 32 | @property 33 | def idx_dp(self) -> int: 34 | return self[RECORD_MAIN_INDEX_ALT][1] 35 | 36 | @property 37 | def id(self) -> int: 38 | return self[RECORD_MAIN_ID] 39 | 40 | @property 41 | def id_hex(self) -> str: 42 | return '0x{sid:08X}'.format(sid=self[RECORD_MAIN_ID]) 43 | 44 | @property 45 | def instance(self) -> int: 46 | return self[RECORD_MAIN_INSTANCE] 47 | 48 | @property 49 | def instance_hex(self) -> str: 50 | return '0x{instance:016X}'.format(instance=self[RECORD_MAIN_INSTANCE]) 51 | 52 | @property 53 | def group(self) -> int: 54 | return self[RECORD_MAIN_GROUP] 55 | 56 | @property 57 | def group_hex(self) -> str: 58 | return '0x{group:08X}'.format(group=self[RECORD_MAIN_GROUP]) 59 | 60 | @property 61 | def source(self) -> str: 62 | return self[RECORD_MAIN_SOURCE] 63 | 64 | @property 65 | def source_old(self) -> str: 66 | return self[RECORD_MAIN_SOURCE_OLD] 67 | 68 | @source_old.setter 69 | def source_old(self, value: str) -> None: 70 | self[RECORD_MAIN_SOURCE_OLD] = value 71 | 72 | @property 73 | def translate(self) -> str: 74 | return self[RECORD_MAIN_TRANSLATE] 75 | 76 | @translate.setter 77 | def translate(self, value: str) -> None: 78 | self[RECORD_MAIN_TRANSLATE] = value 79 | 80 | @property 81 | def translate_old(self) -> str: 82 | return self[RECORD_MAIN_TRANSLATE_OLD] 83 | 84 | @translate_old.setter 85 | def translate_old(self, value: str) -> None: 86 | self[RECORD_MAIN_TRANSLATE_OLD] = value 87 | 88 | @property 89 | def flag(self) -> int: 90 | return self[RECORD_MAIN_FLAG] 91 | 92 | @flag.setter 93 | def flag(self, value: int) -> None: 94 | self[RECORD_MAIN_FLAG] = value 95 | 96 | @property 97 | def resource(self) -> ResourceID: 98 | return self[RECORD_MAIN_RESOURCE] 99 | 100 | @property 101 | def resource_original(self) -> ResourceID: 102 | return self[RECORD_MAIN_RESOURCE_ORIGINAL] 103 | 104 | @property 105 | def package(self) -> str: 106 | return self[RECORD_MAIN_PACKAGE] 107 | 108 | @property 109 | def comment(self) -> str: 110 | return self[RECORD_MAIN_COMMENT] 111 | 112 | @comment.setter 113 | def comment(self, value: str) -> None: 114 | self[RECORD_MAIN_COMMENT] = value 115 | 116 | def compare(self) -> bool: 117 | return compare(self[RECORD_MAIN_SOURCE], self[RECORD_MAIN_TRANSLATE]) 118 | -------------------------------------------------------------------------------- /themes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/themes/__init__.py -------------------------------------------------------------------------------- /themes/dark.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | WINDOW = '#2b2d30' 4 | 5 | TEXT = '#dfe1e5' 6 | TEXT_DISABLED = '#5a5d63' 7 | TEXT_MUTED = '#888786' 8 | TEXT_ERROR = '#de6a66' 9 | 10 | BORDER_LIGHT = '#393b40' 11 | BORDER_DARK = '#4e5157' 12 | BORDER_FOCUS = '#3574f0' 13 | 14 | LINE_EDIT = '#2b2d30' 15 | PLAIN_EDIT = '#1e1f22' 16 | COMBOBOX = '#393b40' 17 | 18 | BUTTON = '#2b2d30' 19 | BUTTON_HOVER = '#393b40' 20 | BUTTON_PRESSED = '#1e1f22' 21 | BUTTON_DISABLED = '#f8f8f8' 22 | BUTTON_DEFAULT = '#3574f0' 23 | BUTTON_DEFAULT_HOVER = '#255ed0' 24 | 25 | SCROLLBAR = '#454547' 26 | SCROLLBAR_HOVER = '#4e4e50' 27 | 28 | SELECTION = '#2e436e' 29 | SELECTION_TEXT = TEXT 30 | 31 | TAB_INACTIVE = PLAIN_EDIT 32 | TAB_ACTIVE = '#2b2d30' 33 | TAB_ACTIVE_BORDER = BORDER_DARK 34 | 35 | TRANSLATED_TABLEVIEW = '#1e1f22' 36 | TRANSLATED_TABLEVIEW_ODD = '#26282e' 37 | TRANSLATED_BAR = '#1e1f22' 38 | 39 | VALIDATED_TABLEVIEW = '#35354e' 40 | VALIDATED_TABLEVIEW_ODD = '#434361' 41 | VALIDATED_BAR = '#8585c2' 42 | 43 | PROGRESS_TABLEVIEW = '#533453' 44 | PROGRESS_TABLEVIEW_ODD = '#684268' 45 | PROGRESS_BAR = '#cf84cf' 46 | 47 | UNVALIDATED_TABLEVIEW = '#592a29' 48 | UNVALIDATED_TABLEVIEW_ODD = '#6f3533' 49 | UNVALIDATED_BAR = '#de6a66' 50 | 51 | DIFFERENT_TABLEVIEW = '#563e2b' 52 | DIFFERENT_TABLEVIEW_ODD = '#6b4d36' 53 | 54 | PROGRESSBAR = PLAIN_EDIT 55 | 56 | EDITOR_LINES = '#26282e' 57 | EDITOR_LINES_TEXT = TEXT_MUTED 58 | EDITOR_LINES_BORDER = BORDER_LIGHT 59 | 60 | EDITOR_LINE = '#26282e' 61 | EDITOR_BRACKET = '#43454a' 62 | 63 | EDITOR_SIMNAME = '#73bd79' 64 | EDITOR_SIMNAME_BOLD = False 65 | 66 | EDITOR_MALE = '#70aeff' 67 | EDITOR_MALE_BOLD = False 68 | 69 | EDITOR_FEMALE = '#cf84cf' 70 | EDITOR_FEMALE_BOLD = False 71 | 72 | EDITOR_VAR_BOLD = True 73 | 74 | EDITOR_TAG = TEXT_MUTED 75 | 76 | HEADER = BORDER_LIGHT 77 | 78 | FONT_SANS = "Roboto, 'Segoe UI', sans-serif" 79 | FONT_MONOSPACE = "'JetBrains Mono', Consolas, 'Courier New', monospace" 80 | -------------------------------------------------------------------------------- /themes/light.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | WINDOW = '#f8f8f8' 4 | 5 | TEXT = '#333' 6 | TEXT_DISABLED = '#b0b0b0' 7 | TEXT_MUTED = '#888786' 8 | TEXT_ERROR = '#de6a66' 9 | 10 | BORDER_LIGHT = '#d3d3d3' 11 | BORDER_DARK = '#cecece' 12 | BORDER_FOCUS = '#0090f1' 13 | 14 | LINE_EDIT = '#fff' 15 | PLAIN_EDIT = '#fff' 16 | COMBOBOX = '#f1f1f1' 17 | 18 | BUTTON = '#f1f1f1' 19 | BUTTON_HOVER = '#ececec' 20 | BUTTON_PRESSED = '#dddddd' 21 | BUTTON_DISABLED = '#f1f1f1' 22 | BUTTON_DEFAULT = '#0090f1' 23 | BUTTON_DEFAULT_HOVER = '#0062a3' 24 | 25 | SCROLLBAR = '#c1c1c1' 26 | SCROLLBAR_HOVER = '#929292' 27 | 28 | SELECTION = '#0060c0' 29 | SELECTION_TEXT = '#fff' 30 | 31 | TAB_INACTIVE = WINDOW 32 | TAB_ACTIVE = '#fff' 33 | TAB_ACTIVE_BORDER = '#0090f1' 34 | 35 | TRANSLATED_TABLEVIEW = '#fff' 36 | TRANSLATED_TABLEVIEW_ODD = '#f1f1f1' 37 | TRANSLATED_BAR = '#fff' 38 | 39 | VALIDATED_TABLEVIEW = '#cecee7' 40 | VALIDATED_TABLEVIEW_ODD = '#c2c2e0' 41 | VALIDATED_BAR = '#c2c2e0' 42 | 43 | PROGRESS_TABLEVIEW = '#eccdec' 44 | PROGRESS_TABLEVIEW_ODD = '#e7c1e7' 45 | PROGRESS_BAR = '#e7c1e7' 46 | 47 | UNVALIDATED_TABLEVIEW = '#f2c3c2' 48 | UNVALIDATED_TABLEVIEW_ODD = '#eeb4b2' 49 | UNVALIDATED_BAR = '#eeb4b2' 50 | 51 | DIFFERENT_TABLEVIEW = '#efd7c4' 52 | DIFFERENT_TABLEVIEW_ODD = '#eaccb5' 53 | 54 | PROGRESSBAR = BUTTON_HOVER 55 | 56 | EDITOR_LINES = '#f1f1f1' 57 | EDITOR_LINES_TEXT = '#b0b0b0' 58 | EDITOR_LINES_BORDER = None 59 | 60 | EDITOR_LINE = WINDOW 61 | EDITOR_BRACKET = BUTTON_PRESSED 62 | 63 | EDITOR_SIMNAME = '#388f21' 64 | EDITOR_SIMNAME_BOLD = True 65 | 66 | EDITOR_MALE = '#0057ad' 67 | EDITOR_MALE_BOLD = True 68 | 69 | EDITOR_FEMALE = '#914c9c' 70 | EDITOR_FEMALE_BOLD = True 71 | 72 | EDITOR_VAR_BOLD = True 73 | 74 | EDITOR_TAG = TEXT_MUTED 75 | 76 | HEADER = '#fff' 77 | 78 | FONT_SANS = "Roboto, 'Segoe UI', sans-serif" 79 | FONT_MONOSPACE = "'JetBrains Mono', Consolas, 'Courier New', monospace" 80 | -------------------------------------------------------------------------------- /themes/stylesheet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from PySide6.QtCore import QFile, QIODevice, QTextStream 5 | 6 | import themes.light as light 7 | import themes.dark as dark 8 | 9 | from singletons.config import config 10 | 11 | 12 | def stylesheet(): 13 | theme_name = config.theme_name 14 | colors = dark if theme_name == 'dark' else light 15 | 16 | colors_dict = { 17 | '__THEME__': theme_name, 18 | 19 | '__WINDOW__': colors.WINDOW, 20 | 21 | '__TEXT__': colors.TEXT, 22 | '__TEXT_DISABLED__': colors.TEXT_DISABLED, 23 | '__TEXT_MUTED__': colors.TEXT_MUTED, 24 | 25 | '__BORDER_LIGHT__': colors.BORDER_LIGHT, 26 | '__BORDER_DARK__': colors.BORDER_DARK, 27 | '__BORDER_FOCUS__': colors.BORDER_FOCUS, 28 | 29 | '__LINE_EDIT__': colors.LINE_EDIT, 30 | '__PLAIN_EDIT__': colors.PLAIN_EDIT, 31 | '__COMBOBOX__': colors.COMBOBOX, 32 | '__PROGRESSBAR__': colors.PROGRESSBAR, 33 | 34 | '__BUTTON__': colors.BUTTON, 35 | '__BUTTON_HOVER__': colors.BUTTON_HOVER, 36 | '__BUTTON_PRESSED__': colors.BUTTON_PRESSED, 37 | '__BUTTON_DISABLED__': colors.BUTTON_DISABLED, 38 | '__BUTTON_DEFAULT__': colors.BUTTON_DEFAULT, 39 | '__BUTTON_DEFAULT_HOVER__': colors.BUTTON_DEFAULT_HOVER, 40 | 41 | '__SCROLLBAR__': colors.SCROLLBAR, 42 | '__SCROLLBAR_HOVER__': colors.SCROLLBAR_HOVER, 43 | 44 | '__SELECTION__': colors.SELECTION, 45 | '__SELECTION_TEXT__': colors.SELECTION_TEXT, 46 | 47 | '__TAB_INACTIVE__': colors.TAB_INACTIVE, 48 | '__TAB_ACTIVE__': colors.TAB_ACTIVE, 49 | '__TAB_ACTIVE_BORDER__': colors.TAB_ACTIVE_BORDER, 50 | 51 | '__TRANSLATED_BAR__': colors.TRANSLATED_BAR, 52 | '__VALIDATED_BAR__': colors.VALIDATED_BAR, 53 | '__PROGRESS_BAR__': colors.PROGRESS_BAR, 54 | '__UNVALIDATED_BAR__': colors.UNVALIDATED_BAR, 55 | 56 | '__HEADER__': colors.HEADER, 57 | 58 | '__FONT_SANS__': colors.FONT_SANS, 59 | '__FONT_MONOSPACE__': colors.FONT_MONOSPACE, 60 | } 61 | 62 | file = QFile(f':/theme.qss') 63 | if file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text): 64 | stream = QTextStream(file) 65 | content = stream.readAll() 66 | file.close() 67 | else: 68 | content = '' 69 | 70 | keys = (re.escape(k) for k in colors_dict.keys()) 71 | pattern = re.compile(r'\b(' + '|'.join(keys) + r')\b') 72 | return pattern.sub(lambda x: colors_dict[x.group()], content) 73 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/utils/__init__.py -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | APP_VERSION = '1.4' 4 | APP_RELEASE_CANDITATE = False 5 | DICTIONARY_VERSION = 4 6 | 7 | COLUMN_MAIN_INDEX = 1 8 | COLUMN_MAIN_ID = 2 9 | COLUMN_MAIN_INSTANCE = 3 10 | COLUMN_MAIN_GROUP = 4 11 | COLUMN_MAIN_SOURCE = 5 12 | COLUMN_MAIN_TRANSLATE = 6 13 | COLUMN_MAIN_COMMENT = 7 14 | COLUMN_MAIN_FLAG = 8 15 | 16 | COLUMN_DICTIONARIES_PACKAGE = 1 17 | COLUMN_DICTIONARIES_SOURCE = 2 18 | COLUMN_DICTIONARIES_TRANSLATE = 3 19 | COLUMN_DICTIONARIES_LENGTH = 4 20 | 21 | RECORD_MAIN_INDEX = 0 22 | RECORD_MAIN_ID = 1 23 | RECORD_MAIN_INSTANCE = 2 24 | RECORD_MAIN_GROUP = 3 25 | RECORD_MAIN_SOURCE = 4 26 | RECORD_MAIN_TRANSLATE = 5 27 | RECORD_MAIN_FLAG = 6 28 | RECORD_MAIN_RESOURCE = 7 29 | RECORD_MAIN_RESOURCE_ORIGINAL = 8 30 | RECORD_MAIN_PACKAGE = 9 31 | RECORD_MAIN_SOURCE_OLD = 10 32 | RECORD_MAIN_TRANSLATE_OLD = 11 33 | RECORD_MAIN_INDEX_ALT = 12 34 | RECORD_MAIN_COMMENT = 13 35 | 36 | RECORD_DICTIONARY_PACKAGE = 0 37 | RECORD_DICTIONARY_SOURCE = 1 38 | RECORD_DICTIONARY_TRANSLATE = 2 39 | RECORD_DICTIONARY_LENGTH = 3 40 | 41 | FLAG_UNVALIDATED = 0 42 | FLAG_PROGRESS = 1 43 | FLAG_VALIDATED = 2 44 | FLAG_TRANSLATED = 3 45 | FLAG_REPLACED = 4 46 | 47 | SEARCH_IN_SOURCE = 0 48 | SEARCH_IN_DESTINATION = 1 49 | SEARCH_IN_ID = 2 50 | 51 | NUMERATION_STANDART = 0 52 | NUMERATION_SOURCE = 1 53 | NUMERATION_XML = 2 54 | NUMERATION_XML_DP = 3 55 | 56 | EXPORT_STBL = 0 57 | EXPORT_XML = 1 58 | EXPORT_XML_DP = 2 59 | EXPORT_JSON_S4S = 3 60 | EXPORT_BINARY_S4S = 4 61 | -------------------------------------------------------------------------------- /utils/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | import hashlib 6 | import tempfile 7 | import shutil 8 | from PySide6.QtWidgets import QFileDialog 9 | from xml.etree import ElementTree 10 | from xml.dom import minidom 11 | 12 | from singletons.config import config 13 | from singletons.interface import interface 14 | 15 | 16 | def static_vars(**kwargs): 17 | def decorate(func): 18 | for k in kwargs: 19 | setattr(func, k, kwargs[k]) 20 | return func 21 | return decorate 22 | 23 | 24 | @static_vars(directory=None) 25 | def opendir(f=None): 26 | if opendir.directory is None: 27 | opendir.directory = f if f else os.path.abspath('.') 28 | dialog = QFileDialog(directory=opendir.directory) 29 | dialog.setFileMode(QFileDialog.FileMode.Directory) 30 | if dialog.exec(): 31 | directory = dialog.selectedFiles()[0] 32 | openfile.directory = directory 33 | return directory 34 | return None 35 | 36 | 37 | @static_vars(directory=None) 38 | def openfile(f, many=False): 39 | if openfile.directory is None: 40 | openfile.directory = config.value('temporary', 'directory') 41 | dialog = QFileDialog(filter=f, directory=openfile.directory) 42 | dialog.setFileMode(QFileDialog.FileMode.ExistingFiles if many else QFileDialog.FileMode.ExistingFile) 43 | if dialog.exec(): 44 | files = dialog.selectedFiles() 45 | openfile.directory = os.path.dirname(files[0]) 46 | config.set_value('temporary', 'directory', openfile.directory) 47 | return files if many else files[0] 48 | return None 49 | 50 | 51 | def savefile(f, suffix, filename='') -> str: 52 | dialog = QFileDialog(directory=filename) 53 | dialog.setDefaultSuffix(suffix) 54 | dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) 55 | dialog.setNameFilters([f]) 56 | return dialog.selectedFiles()[0] if dialog.exec() == QFileDialog.DialogCode.Accepted else None 57 | 58 | 59 | def open_supported(many=False): 60 | formats = [ 61 | interface.text('System', 'All files') + ' (*.package *.stbl *.xml *.json *.binary)', 62 | interface.text('System', 'Packages') + ' (*.package)', 63 | interface.text('System', 'STBL files') + ' (*.stbl)', 64 | interface.text('System', 'XML files') + ' (*.xml)', 65 | interface.text('System', 'JSON files') + ' (*.json)', 66 | interface.text('System', 'Binary files') + ' (*.binary)', 67 | ] 68 | return openfile(';;'.join(formats), many=many) 69 | 70 | 71 | def open_xml(): 72 | formats = [ 73 | interface.text('System', 'XML files') + ' (*.xml)', 74 | ] 75 | return openfile(';;'.join(formats)) 76 | 77 | 78 | def save_xml(filename: str = '') -> str: 79 | return savefile(interface.text('System', 'XML files') + ' (*.xml)', 'xml', filename) 80 | 81 | 82 | def save_stbl(filename: str = '') -> str: 83 | return savefile(interface.text('System', 'STBL files') + ' (*.stbl)', 'STBL', filename) 84 | 85 | 86 | def save_package(filename: str = '') -> str: 87 | return savefile(interface.text('System', 'Packages') + ' (*.package)', 'package', filename) 88 | 89 | 90 | def save_json(filename: str = '') -> str: 91 | return savefile(interface.text('System', 'JSON files') + ' (*.json)', 'json', filename) 92 | 93 | 94 | def save_binary(filename: str = '') -> str: 95 | return savefile(interface.text('System', 'Binary files') + ' (*.binary)', 'binary', filename) 96 | 97 | 98 | def create_temporary_copy(path: str) -> str: 99 | temp_dir = tempfile.gettempdir() 100 | temp_path = os.path.join(temp_dir, os.path.basename(path)) 101 | shutil.copy2(path, temp_path) 102 | return temp_path 103 | 104 | 105 | def text_to_table(text): 106 | if text: 107 | text = text.replace("\r", '') 108 | text = re.sub(r'(\\n)+', ' ', text) 109 | text = re.sub(r'\n+', ' ', text) 110 | return text 111 | return '' 112 | 113 | 114 | def text_to_edit(text): 115 | return re.sub(r'\\n', "\n", text) if text else '' 116 | 117 | 118 | def text_to_stbl(text): 119 | return text.replace("\r", '').replace("\n", '\\n') if text else '' 120 | 121 | 122 | def compare(text1, text2): 123 | return text_to_stbl(text1) == text_to_stbl(text2) 124 | 125 | 126 | def md5(string): 127 | hash_object = hashlib.md5(string.encode('utf-8')) 128 | return hash_object.hexdigest() 129 | 130 | 131 | def _hash(value, init, prime, mask): 132 | if isinstance(value, str): 133 | value = value.lower().encode() 134 | h = init 135 | for byte in value: 136 | h = (h * prime) & mask 137 | h = h ^ byte 138 | return h 139 | 140 | 141 | def fnv32(value): 142 | return _hash(value, 0x811c9dc5, 0x1000193, (1 << 32) - 1) 143 | 144 | 145 | def fnv64(value): 146 | return _hash(value, 0xCBF29CE484222325, 0x100000001b3, (1 << 64) - 1) 147 | 148 | 149 | def prettify(node): 150 | rough = ElementTree.tostring(node, encoding='utf-8').decode('utf-8') 151 | reparsed = minidom.parseString(rough) 152 | return reparsed.toprettyxml(indent=' ', encoding='utf-8') 153 | 154 | 155 | def parsexml(content): 156 | return ''.join((re.sub(r'<(\w+)', r'<\1 DUMMY_LINE="' + str(i + 1) + '"', line) for i, line in enumerate(content))) 157 | -------------------------------------------------------------------------------- /widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/widgets/__init__.py -------------------------------------------------------------------------------- /widgets/colorbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QObject, QThreadPool, QRunnable, Signal, Slot 4 | from PySide6.QtWidgets import QFrame, QHBoxLayout 5 | 6 | from singletons.state import app_state 7 | from singletons.signals import color_signals 8 | from utils.constants import * 9 | 10 | 11 | class UpdateSignals(QObject): 12 | finished = Signal(int, int, int, int) 13 | 14 | 15 | class UpdateWorker(QRunnable): 16 | 17 | def __init__(self, items): 18 | super().__init__() 19 | 20 | self.items = items 21 | 22 | self.signals = UpdateSignals() 23 | 24 | def run(self): 25 | translated_count = 0 26 | validated_count = 0 27 | progess_count = 0 28 | unvalidated_count = 0 29 | 30 | for item in self.items: 31 | flag = item.flag 32 | if flag == FLAG_TRANSLATED: 33 | translated_count += 1 34 | elif flag == FLAG_VALIDATED: 35 | validated_count += 1 36 | elif flag == FLAG_PROGRESS: 37 | progess_count += 1 38 | elif flag == FLAG_UNVALIDATED: 39 | unvalidated_count += 1 40 | 41 | self.signals.finished.emit(translated_count, validated_count, progess_count, unvalidated_count) 42 | 43 | 44 | class TranslatedWidget(QFrame): 45 | pass 46 | 47 | 48 | class ValidatedWidget(QFrame): 49 | pass 50 | 51 | 52 | class ProgressWidget(QFrame): 53 | pass 54 | 55 | 56 | class UnvalidatedWidget(QFrame): 57 | pass 58 | 59 | 60 | class QColorBar(QFrame): 61 | 62 | def __init__(self, parent=None): 63 | super().__init__(parent) 64 | 65 | self.setFrameShape(QFrame.Shape.StyledPanel) 66 | self.setFrameShadow(QFrame.Shadow.Plain) 67 | 68 | self.layout = QHBoxLayout(self) 69 | self.layout.setContentsMargins(0, 0, 0, 0) 70 | self.layout.setSpacing(0) 71 | 72 | self.setFixedHeight(10) 73 | 74 | self.translated = TranslatedWidget(self) 75 | self.validated = ValidatedWidget(self) 76 | self.progress = ProgressWidget(self) 77 | self.unvalidated = UnvalidatedWidget(self) 78 | 79 | self.layout.addWidget(self.translated) 80 | self.layout.addWidget(self.validated) 81 | self.layout.addWidget(self.progress) 82 | self.layout.addWidget(self.unvalidated) 83 | 84 | color_signals.update.connect(self.__update) 85 | 86 | self.__pool = QThreadPool() 87 | 88 | @Slot() 89 | def __update(self): 90 | worker = UpdateWorker(app_state.packages_storage.items()) 91 | worker.setAutoDelete(True) 92 | worker.signals.finished.connect(self.__finished) 93 | self.__pool.start(worker) 94 | 95 | def resfesh(self): 96 | self.__update() 97 | 98 | @Slot(int, int, int, int) 99 | def __finished(self, translated_count, validated_count, progess_count, unvalidated_count): 100 | self.update_colors(translated_count, validated_count, progess_count, unvalidated_count) 101 | 102 | def update_colors(self, translated_count, validated_count, progess_count, unvalidated_count): 103 | self.translated.setVisible(translated_count > 0) 104 | self.validated.setVisible(validated_count > 0) 105 | self.progress.setVisible(progess_count > 0) 106 | self.unvalidated.setVisible(unvalidated_count > 0) 107 | 108 | self.layout.setStretch(0, translated_count) 109 | self.layout.setStretch(1, validated_count) 110 | self.layout.setStretch(2, progess_count) 111 | self.layout.setStretch(3, unvalidated_count) 112 | -------------------------------------------------------------------------------- /widgets/delegate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QRect 4 | from PySide6.QtWidgets import QStyledItemDelegate, QProxyStyle, QStyleOptionHeader, QStyle 5 | from PySide6.QtGui import QColor, QIcon 6 | 7 | import themes.light as light 8 | import themes.dark as dark 9 | 10 | from singletons.config import config 11 | from singletons.state import app_state 12 | from utils.constants import * 13 | 14 | 15 | class MainDelegatePaint(QStyledItemDelegate): 16 | 17 | def __init__(self, parent=None): 18 | super().__init__(parent) 19 | 20 | self.model = app_state.packages_storage.model 21 | self.proxy = app_state.packages_storage.proxy 22 | 23 | is_dark_theme = config.value('interface', 'theme') == 'dark' 24 | 25 | colors_light = { 26 | FLAG_UNVALIDATED: [QColor(light.UNVALIDATED_TABLEVIEW), QColor(light.UNVALIDATED_TABLEVIEW_ODD)], 27 | FLAG_PROGRESS: [QColor(light.PROGRESS_TABLEVIEW), QColor(light.PROGRESS_TABLEVIEW_ODD)], 28 | FLAG_VALIDATED: [QColor(light.VALIDATED_TABLEVIEW), QColor(light.VALIDATED_TABLEVIEW_ODD)], 29 | FLAG_REPLACED: [QColor('#c7ffff'), QColor('#e6ffff')], 30 | FLAG_TRANSLATED: [QColor(light.TRANSLATED_TABLEVIEW), QColor(light.TRANSLATED_TABLEVIEW_ODD)] 31 | } 32 | 33 | colors_dark = { 34 | FLAG_UNVALIDATED: [QColor(dark.UNVALIDATED_TABLEVIEW), QColor(dark.UNVALIDATED_TABLEVIEW_ODD)], 35 | FLAG_PROGRESS: [QColor(dark.PROGRESS_TABLEVIEW), QColor(dark.PROGRESS_TABLEVIEW_ODD)], 36 | FLAG_VALIDATED: [QColor(dark.VALIDATED_TABLEVIEW), QColor(dark.VALIDATED_TABLEVIEW_ODD)], 37 | FLAG_REPLACED: [QColor('#c7ffff'), QColor('#e6ffff')], 38 | FLAG_TRANSLATED: [QColor(dark.TRANSLATED_TABLEVIEW), QColor(dark.TRANSLATED_TABLEVIEW_ODD)] 39 | } 40 | 41 | diffirent_light = [QColor(light.DIFFERENT_TABLEVIEW), QColor(light.DIFFERENT_TABLEVIEW_ODD)] 42 | diffirent_dark = [QColor(dark.DIFFERENT_TABLEVIEW), QColor(dark.DIFFERENT_TABLEVIEW_ODD)] 43 | 44 | self.__colors = colors_dark if is_dark_theme else colors_light 45 | self.__diffirent = diffirent_dark if is_dark_theme else diffirent_light 46 | 47 | def paint(self, painter, option, index): 48 | try: 49 | row = self.proxy.mapToSource(index).row() 50 | column = index.column() 51 | 52 | if 0 <= row < len(self.model.filtered): 53 | item = self.model.filtered[row] 54 | 55 | remainder = index.row() % 2 56 | if (column == COLUMN_MAIN_SOURCE and item.source_old or 57 | column == COLUMN_MAIN_TRANSLATE and item.translate_old): 58 | color = self.__diffirent[remainder] 59 | else: 60 | color = self.__colors[item.flag][remainder] 61 | 62 | painter.fillRect(option.rect, color) 63 | 64 | except IndexError: 65 | pass 66 | 67 | super().paint(painter, option, index) 68 | 69 | 70 | class DictionaryDelegatePaint(QStyledItemDelegate): 71 | 72 | def __init__(self, parent=None): 73 | super().__init__(parent) 74 | 75 | is_dark_theme = config.value('interface', 'theme') == 'dark' 76 | 77 | colors_light = [QColor(light.TRANSLATED_TABLEVIEW), QColor(light.TRANSLATED_TABLEVIEW_ODD)] 78 | colors_dark = [QColor(dark.TRANSLATED_TABLEVIEW), QColor(dark.TRANSLATED_TABLEVIEW_ODD)] 79 | 80 | self.__colors = colors_dark if is_dark_theme else colors_light 81 | 82 | def paint(self, painter, option, index): 83 | painter.fillRect(option.rect, self.__colors[index.row() % 2]) 84 | super().paint(painter, option, index) 85 | 86 | 87 | class HeaderProxy(QProxyStyle): 88 | 89 | def __init__(self, parent=None): 90 | super().__init__(parent) 91 | 92 | self.theme_name = config.value('interface', 'theme') 93 | 94 | self.text_color = QColor(dark.TEXT) if self.theme_name == 'dark' else QColor(light.TEXT) 95 | 96 | def drawControl(self, element, option, painter, widget=None): 97 | if element == QStyle.ControlElement.CE_HeaderLabel: 98 | sort_option = option.sortIndicator 99 | rect = option.rect 100 | 101 | text_width = option.fontMetrics.horizontalAdvance(option.text) 102 | text_height = option.fontMetrics.height() 103 | text_rect = QRect(rect.left() + (rect.width() - text_width) / 2, 104 | rect.top() + 1 + (rect.height() - text_height) / 2, 105 | text_width, text_height) 106 | painter.setPen(self.text_color) 107 | painter.drawText(text_rect, option.text) 108 | 109 | sort_icon = None 110 | if sort_option == QStyleOptionHeader.SortIndicator.SortDown: 111 | sort_icon = QIcon(f':/images/{self.theme_name}/arrow_down.png').pixmap(10, 6) 112 | elif sort_option == QStyleOptionHeader.SortIndicator.SortUp: 113 | sort_icon = QIcon(f':/images/{self.theme_name}/arrow_up.png').pixmap(10, 6) 114 | 115 | if sort_icon: 116 | sort_rect = QRect(rect.left() + (rect.width() - sort_icon.width()) / 2, 117 | rect.top(), sort_icon.width(), sort_icon.height()) 118 | painter.drawPixmap(sort_rect, sort_icon) 119 | 120 | else: 121 | super().drawControl(element, option, painter, widget) 122 | -------------------------------------------------------------------------------- /widgets/editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from PySide6.QtCore import Qt, QRect, QSize, QObject, Signal 5 | from PySide6.QtWidgets import QWidget, QPlainTextEdit, QTextEdit, QMenu 6 | from PySide6.QtGui import QColor, QFont, QSyntaxHighlighter, QTextCursor, QPainter, QTextCharFormat, \ 7 | QTextFormat, QTextOption, QIcon, QPen 8 | 9 | import themes.light as light 10 | import themes.dark as dark 11 | 12 | from singletons.config import config 13 | from singletons.interface import interface 14 | 15 | 16 | class LineNumberArea(QWidget): 17 | 18 | def __init__(self, editor): 19 | super().__init__(editor) 20 | self.editor = editor 21 | 22 | def sizeHint(self): 23 | return QSize(self.editor.lineNumberAreaWidth(), 0) 24 | 25 | def paintEvent(self, event): 26 | self.editor.lineNumberAreaPaintEvent(event) 27 | 28 | 29 | class QTextEditor(QPlainTextEdit): 30 | 31 | selected = Signal(QObject) 32 | 33 | def __init__(self, parent=None): 34 | super().__init__(parent) 35 | 36 | self.lineNumberArea = LineNumberArea(self) 37 | self.blockCountChanged.connect(self.updateLineNumberAreaWidth) 38 | self.updateRequest.connect(self.updateLineNumberArea) 39 | self.cursorPositionChanged.connect(self.highlightCurrentLine) 40 | self.cursorPositionChanged.connect(self.highlightMatchingBracket) 41 | 42 | self.updateLineNumberAreaWidth(0) 43 | 44 | option = QTextOption() 45 | option.setFlags(QTextOption.Flag.ShowTabsAndSpaces | QTextOption.Flag.AddSpaceForLineAndParagraphSeparators) 46 | self.document().setDefaultTextOption(option) 47 | 48 | self.highlighter = BracketHighlighter(self.document()) 49 | 50 | is_dark_theme = config.value('interface', 'theme') == 'dark' 51 | 52 | self.lines_color = QColor(dark.EDITOR_LINES) if is_dark_theme else QColor(light.EDITOR_LINES) 53 | self.lines_text = QColor(dark.EDITOR_LINES_TEXT) if is_dark_theme else QColor(light.EDITOR_LINES_TEXT) 54 | 55 | lines_border = dark.EDITOR_LINES_BORDER if is_dark_theme else light.EDITOR_LINES_BORDER 56 | self.lines_border = QColor(lines_border) if lines_border else None 57 | 58 | self.line_color = QColor(dark.EDITOR_LINE) if is_dark_theme else QColor(light.EDITOR_LINE) 59 | self.bracket_color = QColor(dark.EDITOR_BRACKET) if is_dark_theme else QColor(light.EDITOR_BRACKET) 60 | 61 | def contextMenuEvent(self, event): 62 | menu = QMenu(self) 63 | 64 | undo_action = menu.addAction(interface.text('TextEditor', 'Undo')) 65 | undo_action.setShortcut('Ctrl+Z') 66 | 67 | redo_action = menu.addAction(interface.text('TextEditor', 'Redo')) 68 | redo_action.setShortcut('Ctrl+Shift+Z') 69 | 70 | menu.addSeparator() 71 | 72 | cut_action = menu.addAction(interface.text('TextEditor', 'Cut')) 73 | cut_action.setShortcut('Ctrl+X') 74 | 75 | copy_action = menu.addAction(QIcon(':/images/copy.png'), interface.text('TextEditor', 'Copy')) 76 | copy_action.setShortcut('Ctrl+C') 77 | 78 | paste_action = menu.addAction(QIcon(':/images/paste.png'), interface.text('TextEditor', 'Paste')) 79 | paste_action.setShortcut('Ctrl+V') 80 | 81 | menu.addSeparator() 82 | 83 | select_all_action = menu.addAction(interface.text('TextEditor', 'Select All')) 84 | select_all_action.setShortcut('Ctrl+A') 85 | 86 | undo_action.triggered.connect(self.undo) 87 | redo_action.triggered.connect(self.redo) 88 | cut_action.triggered.connect(self.cut) 89 | copy_action.triggered.connect(self.copy) 90 | paste_action.triggered.connect(self.paste) 91 | select_all_action.triggered.connect(self.selectAll) 92 | 93 | menu.exec_(event.globalPos()) 94 | 95 | def lineNumberAreaWidth(self): 96 | digits = 1 97 | max_value = max(1, self.blockCount()) 98 | while max_value >= 10: 99 | max_value /= 10 100 | digits += 1 101 | space = 14 + self.fontMetrics().horizontalAdvance('9') * digits 102 | return space 103 | 104 | def updateLineNumberAreaWidth(self, _): 105 | self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0) 106 | 107 | def updateLineNumberArea(self, rect, dy): 108 | if dy: 109 | self.lineNumberArea.scroll(0, dy) 110 | else: 111 | self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height()) 112 | 113 | if rect.contains(self.viewport().rect()): 114 | self.updateLineNumberAreaWidth(0) 115 | 116 | def resizeEvent(self, event): 117 | super().resizeEvent(event) 118 | 119 | cr = self.contentsRect() 120 | self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), 121 | self.lineNumberAreaWidth(), cr.height())) 122 | 123 | def lineNumberAreaPaintEvent(self, event): 124 | painter = QPainter(self.lineNumberArea) 125 | painter.fillRect(event.rect(), self.lines_color) 126 | 127 | if self.lines_border: 128 | painter.setPen(QPen(self.lines_border, 1)) 129 | rect = event.rect() 130 | painter.drawLine(rect.right(), rect.top(), rect.right(), rect.bottom()) 131 | 132 | block = self.firstVisibleBlock() 133 | block_number = block.blockNumber() 134 | top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() 135 | bottom = top + self.blockBoundingRect(block).height() 136 | 137 | while block.isValid() and top <= event.rect().bottom(): 138 | if block.isVisible() and bottom >= event.rect().top(): 139 | number = str(block_number + 1) 140 | painter.setPen(self.lines_text) 141 | painter.drawText(0, top, self.lineNumberArea.width() - 8, self.fontMetrics().height(), 142 | Qt.AlignmentFlag.AlignRight, number) 143 | 144 | block = block.next() 145 | top = bottom 146 | bottom = top + self.blockBoundingRect(block).height() 147 | block_number += 1 148 | 149 | def highlightCurrentLine(self): 150 | extra_selections = [] 151 | 152 | if not self.isReadOnly(): 153 | selection = QTextEdit.ExtraSelection() 154 | 155 | selection.format.setBackground(self.line_color) 156 | selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True) 157 | selection.cursor = self.textCursor() 158 | selection.cursor.clearSelection() 159 | 160 | extra_selections.append(selection) 161 | 162 | self.setExtraSelections(extra_selections) 163 | 164 | def highlightMatchingBracket(self): 165 | cursor = self.textCursor() 166 | block = cursor.block() 167 | text = block.text() 168 | pos = cursor.positionInBlock() 169 | char_before = text[pos-1] if pos > 0 else '' 170 | char_after = text[pos] if pos < len(text) else '' 171 | 172 | extra_selections = self.extraSelections() 173 | 174 | if not self.isReadOnly(): 175 | for bracket in ('<>', '{}'): 176 | open_bracket = bracket[0] 177 | close_bracket = bracket[1] 178 | 179 | if char_before == open_bracket or char_after == open_bracket: 180 | open_pos = pos - 1 if char_before == open_bracket else pos 181 | close_pos = self.findMatchingBracket(block, open_pos, open_bracket, close_bracket) 182 | if close_pos != -1: 183 | extra_selections.append(self.createBracketSelection(cursor, open_pos)) 184 | extra_selections.append(self.createBracketSelection(cursor, close_pos)) 185 | 186 | elif char_before == close_bracket or char_after == close_bracket: 187 | close_pos = pos - 1 if char_before == close_bracket else pos 188 | open_pos = self.findMatchingBracket(block, close_pos, close_bracket, open_bracket, reverse=True) 189 | if open_pos != -1: 190 | extra_selections.append(self.createBracketSelection(cursor, open_pos)) 191 | extra_selections.append(self.createBracketSelection(cursor, close_pos)) 192 | 193 | self.setExtraSelections(extra_selections) 194 | 195 | def findMatchingBracket(self, block, pos, open_bracket, close_bracket, reverse=False): 196 | text = block.text() 197 | direction = -1 if reverse else 1 198 | stack = 0 199 | 200 | while 0 <= pos < len(text): 201 | char = text[pos] 202 | if char == open_bracket: 203 | stack += 1 204 | elif char == close_bracket: 205 | stack -= 1 206 | if stack == 0: 207 | return pos 208 | pos += direction 209 | 210 | return -1 211 | 212 | def createBracketSelection(self, cursor, pos): 213 | selection = QTextEdit.ExtraSelection() 214 | 215 | fmt = QTextCharFormat() 216 | fmt.setBackground(self.bracket_color) 217 | selection.format = fmt 218 | 219 | new_cursor = self.textCursor() 220 | new_cursor.setPosition(cursor.block().position() + pos) 221 | new_cursor.movePosition(QTextCursor.MoveOperation.NextCharacter, QTextCursor.MoveMode.KeepAnchor) 222 | selection.cursor = new_cursor 223 | 224 | return selection 225 | 226 | def mouseReleaseEvent(self, e): 227 | super().mouseReleaseEvent(e) 228 | self.selected.emit(self) 229 | 230 | 231 | class BracketHighlighter(QSyntaxHighlighter): 232 | 233 | def __init__(self, document): 234 | super().__init__(document) 235 | 236 | is_dark_theme = config.value('interface', 'theme') == 'dark' 237 | 238 | patterns_light = [ 239 | (re.compile(r'{\w+\.[^}]+}'), None, True), 240 | (re.compile(r'{[Mm]\w+\.[^}]+}'), QColor(light.EDITOR_MALE), light.EDITOR_MALE_BOLD), 241 | (re.compile(r'{[Ff]\w+\.[^}]+}'), QColor(light.EDITOR_FEMALE), light.EDITOR_FEMALE_BOLD), 242 | (re.compile(r'{\d+\.([Ss]im)[^}]+}'), QColor(light.EDITOR_SIMNAME), light.EDITOR_SIMNAME_BOLD), 243 | (re.compile(r'<[^>]+>'), QColor(light.EDITOR_TAG), False), 244 | (re.compile(r'(\s)'), QColor(0, 0, 0, 0), False), 245 | (re.compile(r'(\s\s+)'), QColor(light.EDITOR_TAG), False) 246 | ] 247 | 248 | patterns_dark = [ 249 | (re.compile(r'{\w+\.[^}]+}'), QColor('#fff'), True), 250 | (re.compile(r'{[Mm]\w+\.[^}]+}'), QColor(dark.EDITOR_MALE), dark.EDITOR_MALE_BOLD), 251 | (re.compile(r'{[Ff]\w+\.[^}]+}'), QColor(dark.EDITOR_FEMALE), dark.EDITOR_FEMALE_BOLD), 252 | (re.compile(r'{\d+\.([Ss]im)[^}]+}'), QColor(dark.EDITOR_SIMNAME), dark.EDITOR_SIMNAME_BOLD), 253 | (re.compile(r'<[^>]+>'), QColor(dark.EDITOR_TAG), False), 254 | (re.compile(r'(\s)'), QColor(0, 0, 0, 0), False), 255 | (re.compile(r'(\s\s+)'), QColor(dark.EDITOR_TAG), False) 256 | ] 257 | 258 | self.__patterns = patterns_dark if is_dark_theme else patterns_light 259 | 260 | def highlightBlock(self, text): 261 | for pattern, color, bold in self.__patterns: 262 | for match in pattern.finditer(text): 263 | start, end = match.span() 264 | self.setFormat(start, end - start, self.getFormat(color, bold)) 265 | 266 | def getFormat(self, color=None, bold=False): 267 | fmt = QTextCharFormat() 268 | if bold: 269 | fmt.setFontWeight(QFont.Weight.Bold) 270 | if color: 271 | fmt.setForeground(color) 272 | return fmt 273 | -------------------------------------------------------------------------------- /widgets/lineedit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtWidgets import QLineEdit, QProxyStyle, QStyle 4 | from PySide6.QtGui import QIcon, QPixmap 5 | 6 | from singletons.config import config 7 | 8 | 9 | class CustomProxyStyle(QProxyStyle): 10 | def __init__(self, parent=None): 11 | super().__init__(parent) 12 | 13 | self.theme_name = config.value('interface', 'theme') 14 | 15 | def standardIcon(self, standardIcon, option=None, widget=None): 16 | if standardIcon == QStyle.StandardPixmap.SP_LineEditClearButton: 17 | return QIcon(QPixmap(f':/images/{self.theme_name}/backspace.png')) 18 | return super().standardIcon(standardIcon, option, widget) 19 | 20 | 21 | class QCleaningLineEdit(QLineEdit): 22 | 23 | def __init__(self, parent=None): 24 | super().__init__(parent) 25 | 26 | self.setStyle(CustomProxyStyle()) 27 | -------------------------------------------------------------------------------- /widgets/tableview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import Qt 4 | from PySide6.QtWidgets import QTableView, QAbstractScrollArea, QAbstractItemView, QHeaderView 5 | 6 | from .delegate import MainDelegatePaint, DictionaryDelegatePaint, HeaderProxy 7 | 8 | from singletons.config import config 9 | from singletons.state import app_state 10 | from utils.constants import * 11 | 12 | 13 | class AbstractTableView(QTableView): 14 | 15 | def __init__(self, parent=None): 16 | super().__init__(parent) 17 | 18 | self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 19 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 20 | self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) 21 | 22 | self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 23 | self.setShowGrid(False) 24 | self.setGridStyle(Qt.PenStyle.NoPen) 25 | self.setSortingEnabled(True) 26 | self.setWordWrap(False) 27 | 28 | header = self.verticalHeader() 29 | header.setSectionResizeMode(QHeaderView.ResizeMode.Fixed) 30 | header.setDefaultSectionSize(26) 31 | header.setVisible(False) 32 | 33 | header = self.horizontalHeader() 34 | header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) 35 | header.setSortIndicator(0, Qt.SortOrder.AscendingOrder) 36 | header.setHighlightSections(False) 37 | header.setStyle(HeaderProxy()) 38 | 39 | def selected_item(self): 40 | model = self.model() 41 | sources = self.selectionModel().selectedRows() 42 | return model.sourceModel().filtered[model.mapToSource(sources[0]).row()] if sources else None 43 | 44 | def selected_items(self): 45 | model = self.model() 46 | sources = self.selectionModel().selectedRows() 47 | return [model.sourceModel().filtered[model.mapToSource(s).row()] for s in sources] if sources else [] 48 | 49 | def refresh(self): 50 | self.model().layoutChanged.emit() 51 | 52 | def resort(self): 53 | header = self.horizontalHeader() 54 | self.model().sourceModel().sort(header.sortIndicatorSection(), header.sortIndicatorOrder()) 55 | 56 | 57 | class QMainTableView(AbstractTableView): 58 | 59 | def __init__(self, parent=None): 60 | super().__init__(parent) 61 | 62 | self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked) 63 | 64 | self.sortByColumn(COLUMN_MAIN_INDEX, Qt.SortOrder.AscendingOrder) 65 | 66 | def set_model(self): 67 | self.setModel(app_state.packages_storage.proxy) 68 | self.setItemDelegate(MainDelegatePaint()) 69 | self.resize_columns() 70 | self.hide_columns() 71 | 72 | def resize_columns(self): 73 | header = self.horizontalHeader() 74 | 75 | header.setSectionResizeMode(COLUMN_MAIN_TRANSLATE, QHeaderView.ResizeMode.Stretch) 76 | 77 | self.setColumnWidth(COLUMN_MAIN_INDEX, 50) 78 | self.setColumnWidth(COLUMN_MAIN_INSTANCE, 160) 79 | self.setColumnWidth(COLUMN_MAIN_SOURCE, 300) 80 | self.setColumnWidth(COLUMN_MAIN_COMMENT, 175) 81 | self.setColumnWidth(COLUMN_MAIN_FLAG, 50) 82 | 83 | self.setColumnHidden(0, True) 84 | 85 | def hide_columns(self): 86 | self.setColumnHidden(COLUMN_MAIN_ID, not config.value('view', 'id')) 87 | self.setColumnHidden(COLUMN_MAIN_INSTANCE, not config.value('view', 'instance')) 88 | self.setColumnHidden(COLUMN_MAIN_GROUP, not config.value('view', 'group')) 89 | self.setColumnHidden(COLUMN_MAIN_SOURCE, not config.value('view', 'source')) 90 | self.setColumnHidden(COLUMN_MAIN_COMMENT, not config.value('view', 'comment')) 91 | 92 | 93 | class QDictionaryTableView(AbstractTableView): 94 | 95 | def __init__(self, parent=None): 96 | super().__init__(parent) 97 | 98 | self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) 99 | self.setEditTriggers(QAbstractItemView.EditTrigger.SelectedClicked) 100 | 101 | self.sortByColumn(COLUMN_DICTIONARIES_LENGTH, Qt.SortOrder.AscendingOrder) 102 | 103 | def set_model(self): 104 | self.setModel(app_state.dictionaries_storage.proxy) 105 | self.setItemDelegate(DictionaryDelegatePaint()) 106 | self.resize_columns() 107 | 108 | def resize_columns(self): 109 | header = self.horizontalHeader() 110 | 111 | header.setSectionResizeMode(COLUMN_DICTIONARIES_TRANSLATE, QHeaderView.ResizeMode.Stretch) 112 | 113 | self.setColumnWidth(COLUMN_DICTIONARIES_PACKAGE, 125) 114 | self.setColumnWidth(COLUMN_DICTIONARIES_SOURCE, 200) 115 | self.setColumnWidth(COLUMN_DICTIONARIES_LENGTH, 5) 116 | 117 | self.setColumnHidden(0, True) 118 | -------------------------------------------------------------------------------- /widgets/toolbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import Qt, QSize 4 | from PySide6.QtWidgets import QToolBar as ToolBar, QComboBox, QWidget, QSizePolicy 5 | from PySide6.QtGui import QAction, QIcon 6 | 7 | from widgets.lineedit import QCleaningLineEdit 8 | 9 | from singletons.interface import interface 10 | 11 | 12 | class QToolBar(ToolBar): 13 | 14 | def __init__(self, parent=None): 15 | super().__init__(parent) 16 | 17 | self.setMovable(False) 18 | self.setIconSize(QSize(18, 18)) 19 | self.setFloatable(False) 20 | self.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu) 21 | 22 | self.search_toggle = QAction(QIcon(':/images/search_source'), None) 23 | 24 | self.filter_validate_3 = QAction(QIcon(':/images/validate_3'), None) 25 | self.filter_validate_3.setCheckable(True) 26 | self.filter_validate_3.setChecked(True) 27 | 28 | self.filter_validate_0 = QAction(QIcon(':/images/validate_0'), None) 29 | self.filter_validate_0.setCheckable(True) 30 | self.filter_validate_0.setChecked(True) 31 | 32 | self.filter_validate_2 = QAction(QIcon(':/images/validate_2'), None) 33 | self.filter_validate_2.setCheckable(True) 34 | self.filter_validate_2.setChecked(True) 35 | 36 | self.filter_validate_1 = QAction(QIcon(':/images/validate_1'), None) 37 | self.filter_validate_1.setCheckable(True) 38 | self.filter_validate_1.setChecked(True) 39 | 40 | self.filter_validate_4 = QAction(QIcon(':/images/validate_4'), None) 41 | self.filter_validate_4.setCheckable(True) 42 | 43 | self.edt_search = FixedLineEdit() 44 | self.cb_files = FilesComboBox() 45 | self.cb_instances = InstancesComboBox() 46 | 47 | self.addSeparator() 48 | self.addWidget(self.edt_search) 49 | self.addSeparator() 50 | self.addAction(self.search_toggle) 51 | 52 | self.addSeparator() 53 | 54 | self.addAction(self.filter_validate_3) 55 | self.addAction(self.filter_validate_0) 56 | self.addAction(self.filter_validate_2) 57 | self.addAction(self.filter_validate_1) 58 | 59 | self.addSeparator() 60 | 61 | self.addAction(self.filter_validate_4) 62 | 63 | self.addSeparator() 64 | 65 | spacer = QWidget() 66 | spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 67 | 68 | self.addWidget(spacer) 69 | 70 | self.addWidget(self.cb_files) 71 | self.addSeparator() 72 | self.addWidget(self.cb_instances) 73 | self.addSeparator() 74 | 75 | self.retranslate() 76 | 77 | def retranslate(self): 78 | self.search_toggle.setToolTip(interface.text('ToolBar', 'Search in original')) 79 | self.filter_validate_0.setToolTip(interface.text('ToolBar', 'Not translated')) 80 | self.filter_validate_1.setToolTip(interface.text('ToolBar', 'Partial translation')) 81 | self.filter_validate_2.setToolTip(interface.text('ToolBar', 'Validated translation')) 82 | self.filter_validate_3.setToolTip(interface.text('ToolBar', 'Translated')) 83 | self.filter_validate_4.setToolTip(interface.text('ToolBar', 'Different strings')) 84 | 85 | self.edt_search.setPlaceholderText(interface.text('ToolBar', 'Search...')) 86 | self.cb_instances.setItemText(0, interface.text('ToolBar', '-- All instances --')) 87 | self.cb_files.setItemText(0, interface.text('ToolBar', '-- All files --')) 88 | 89 | 90 | class FixedLineEdit(QCleaningLineEdit): 91 | 92 | def __init__(self, parent=None): 93 | super().__init__(parent) 94 | 95 | self.adjusted_size = 200 96 | 97 | self.setClearButtonEnabled(True) 98 | self.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)) 99 | self.setContentsMargins(0, 0, 0, 0) 100 | 101 | def sizeHint(self): 102 | return self.minimumSizeHint() 103 | 104 | def minimumSizeHint(self): 105 | return QSize(self.adjusted_size, 26) 106 | 107 | def keyReleaseEvent(self, event): 108 | if event.key() == Qt.Key.Key_Escape: 109 | self.clear() 110 | 111 | 112 | class InstancesComboBox(QComboBox): 113 | 114 | def __init__(self, parent=None): 115 | super().__init__(parent) 116 | 117 | self.adjusted_size = 200 118 | 119 | self.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)) 120 | self.setContentsMargins(0, 0, 0, 0) 121 | 122 | self.addItem('') 123 | 124 | def sizeHint(self): 125 | return self.minimumSizeHint() 126 | 127 | def minimumSizeHint(self): 128 | return QSize(self.adjusted_size, 26) 129 | 130 | 131 | class FilesComboBox(QComboBox): 132 | 133 | def __init__(self, parent=None): 134 | super().__init__(parent) 135 | 136 | self.adjusted_size = 470 137 | 138 | size_policy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) 139 | 140 | self.setSizePolicy(size_policy) 141 | self.setContentsMargins(0, 0, 0, 0) 142 | 143 | self.addItem('') 144 | 145 | def sizeHint(self): 146 | return self.minimumSizeHint() 147 | 148 | def minimumSizeHint(self): 149 | return QSize(self.adjusted_size, 26) 150 | -------------------------------------------------------------------------------- /windows/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/windows/__init__.py -------------------------------------------------------------------------------- /windows/edit_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import Qt, QObject, Slot 4 | from PySide6.QtWidgets import QApplication, QDialog, QMenu 5 | from PySide6.QtGui import QIcon 6 | 7 | from .ui.edit_dialog import Ui_EditDialog 8 | 9 | import themes.light as light 10 | import themes.dark as dark 11 | 12 | from singletons.config import config 13 | from singletons.interface import interface 14 | from singletons.signals import color_signals, storage_signals 15 | from singletons.state import app_state 16 | from singletons.translator import translator 17 | from singletons.undo import undo 18 | from utils.functions import text_to_edit, text_to_stbl 19 | from utils.constants import * 20 | 21 | 22 | class EditDialog(QDialog, Ui_EditDialog): 23 | 24 | def __init__(self, parent=None): 25 | super().__init__(parent) 26 | self.setupUi(self) 27 | 28 | self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint) 29 | 30 | self.item = None 31 | 32 | self.tableview.clicked.connect(self.tableview_click) 33 | self.tableview.customContextMenuRequested.connect(self.generate_item_context_menu) 34 | 35 | self.btn_ok.clicked.connect(self.ok_click) 36 | self.btn_cancel.clicked.connect(self.cancel_click) 37 | 38 | self.cb_api.currentTextChanged.connect(self.change_api) 39 | 40 | self.btn_translate.clicked.connect(self.translate_click) 41 | 42 | self.txt_original.selected.connect(self.selection_change) 43 | self.txt_original_diff.selected.connect(self.selection_change) 44 | 45 | app_state.dictionaries_storage.signals.updated.connect(self.__dictionaries_updated) 46 | storage_signals.updated.connect(self.__dictionaries_updated) 47 | 48 | self.tableview.set_model() 49 | 50 | self.retranslate() 51 | 52 | def retranslate(self): 53 | self.setWindowTitle(interface.text('EditWindow', 'Search and Edit')) 54 | self.btn_translate.setText(interface.text('EditWindow', 'Translate')) 55 | self.btn_ok.setText(interface.text('EditWindow', 'OK (Ctrl+Enter)')) 56 | self.lbl_original.setText(interface.text('EditWindow', 'Original text')) 57 | self.lbl_original_diff.setText(interface.text('EditWindow', 'Different original')) 58 | self.lbl_translate.setText(interface.text('EditWindow', 'Current translation')) 59 | self.lbl_translate_diff.setText(interface.text('EditWindow', 'Different translation')) 60 | self.btn_cancel.setText(interface.text('EditWindow', 'Cancel')) 61 | self.txt_comment.setPlaceholderText(interface.text('EditWindow', 'Comment...')) 62 | 63 | def showEvent(self, event): 64 | self.txt_translate.setFocus() 65 | 66 | def keyPressEvent(self, event): 67 | if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return]: 68 | if event.modifiers() and Qt.KeyboardModifier.ControlModifier: 69 | self.ok_click() 70 | elif event.key() == Qt.Key.Key_Escape: 71 | self.close() 72 | else: 73 | super().keyPressEvent(event) 74 | 75 | @Slot() 76 | def __dictionaries_updated(self): 77 | app_state.dictionaries_storage.proxy.process_filter() 78 | 79 | def change_api(self): 80 | config.set_value('api', 'engine', self.cb_api.currentText()) 81 | 82 | @Slot(QObject) 83 | def selection_change(self, sender): 84 | text = sender.textCursor().selectedText() 85 | if len(text) >= 3: 86 | app_state.dictionaries_storage.proxy.filter(text=text) 87 | 88 | def tableview_click(self, index): 89 | model = self.tableview.model() 90 | item = model.sourceModel().filtered[model.mapToSource(index).row()] 91 | if item: 92 | if index.column() == COLUMN_DICTIONARIES_TRANSLATE: 93 | text = item[RECORD_DICTIONARY_TRANSLATE] 94 | else: 95 | text = item[RECORD_DICTIONARY_SOURCE] 96 | self.txt_search.setPlainText(text_to_edit(text)) 97 | 98 | def prepare(self, item): 99 | self.item = item 100 | 101 | self.txt_search.setPlainText('') 102 | 103 | self.txt_original.setPlainText(text_to_edit(item.source)) 104 | self.txt_translate.setPlainText(text_to_edit(item.translate)) 105 | 106 | self.txt_comment.setText(item.comment) 107 | 108 | engine = config.value('api', 'engine') 109 | self.cb_api.clear() 110 | self.cb_api.addItems(translator.engines) 111 | engine_index = self.cb_api.findText(engine) 112 | self.cb_api.setCurrentIndex(engine_index if engine_index >= 0 else 0) 113 | 114 | if item.source_old: 115 | self.txt_original_diff.setPlainText(text_to_edit(item.source_old)) 116 | self.txt_original_diff.setVisible(True) 117 | self.lbl_original_diff.setVisible(True) 118 | else: 119 | self.txt_original_diff.setVisible(False) 120 | self.lbl_original_diff.setVisible(False) 121 | 122 | if item.translate_old: 123 | self.txt_translate_diff.setPlainText(text_to_edit(item.translate_old)) 124 | self.txt_translate_diff.setVisible(True) 125 | self.lbl_translate_diff.setVisible(True) 126 | else: 127 | self.txt_translate_diff.setVisible(False) 128 | self.lbl_translate_diff.setVisible(False) 129 | 130 | self.txt_resource.setText('Record: STBL - 0x{instance:016x}[0x{id:08x}]'.format(instance=item.resource.instance, 131 | id=item.id)) 132 | 133 | def ok_click(self): 134 | undo.wrap(self.item) 135 | undo.commit() 136 | 137 | self.item.translate = text_to_stbl(self.txt_translate.toPlainText()) 138 | self.item.flag = FLAG_VALIDATED 139 | self.item.comment = self.txt_comment.text() 140 | 141 | self.item.translate_old = None 142 | 143 | app_state.dictionaries_storage.update(self.item) 144 | 145 | color_signals.update.emit() 146 | 147 | self.close() 148 | 149 | def translate_click(self): 150 | self.lbl_status.setStyleSheet('') 151 | self.lbl_status.setText(interface.text('EditWindow', 'Loading...')) 152 | QApplication.processEvents() 153 | response = translator.translate(self.cb_api.currentText(), self.item.source) 154 | if response.status_code == 200: 155 | self.txt_translate.setPlainText(text_to_edit(response.text)) 156 | self.lbl_status.setText('') 157 | else: 158 | color = dark.TEXT_ERROR if config.value('interface', 'theme') == 'dark' else light.TEXT_ERROR 159 | self.lbl_status.setStyleSheet(f'color: {color};') 160 | self.lbl_status.setText(response.text) 161 | 162 | def cancel_click(self): 163 | self.close() 164 | 165 | def generate_item_context_menu(self, position): 166 | index = self.sender().indexAt(position) 167 | if not index.isValid(): 168 | return 169 | 170 | position.setY(position.y() + 22) 171 | 172 | context_menu = QMenu() 173 | 174 | use_action = context_menu.addAction(QIcon(':/images/validate_2.png'), 175 | interface.text('EditWindow', 'Use this translation')) 176 | 177 | action = context_menu.exec_(self.sender().mapToGlobal(position)) 178 | if action is None: 179 | return 180 | 181 | if action == use_action: 182 | item = self.tableview.selected_item() 183 | self.txt_translate.setPlainText(text_to_edit(item[RECORD_DICTIONARY_TRANSLATE])) 184 | -------------------------------------------------------------------------------- /windows/import_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import Qt 4 | from PySide6.QtWidgets import QDialog 5 | 6 | from .ui.import_dialog import Ui_ImportDialog 7 | 8 | from singletons.interface import interface 9 | from singletons.signals import progress_signals, color_signals 10 | from singletons.state import app_state 11 | from singletons.undo import undo 12 | from utils.functions import compare, text_to_stbl 13 | from utils.constants import * 14 | 15 | 16 | class ImportDialog(QDialog, Ui_ImportDialog): 17 | 18 | def __init__(self, parent=None): 19 | super().__init__(parent) 20 | self.setupUi(self) 21 | 22 | self.filename = None 23 | 24 | self.btn_import.clicked.connect(self.import_click) 25 | self.btn_cancel.clicked.connect(self.cancel_click) 26 | 27 | self.retranslate() 28 | 29 | def retranslate(self): 30 | self.setWindowTitle(interface.text('ImportDialog', 'Import translate')) 31 | self.gb_overwrite.setTitle(interface.text('ImportDialog', 'Overwrite')) 32 | self.rb_all.setText(interface.text('ImportDialog', 'Everything')) 33 | self.rb_validated.setText(interface.text('ImportDialog', 'Everything but already validated strings')) 34 | self.rb_validated_partial.setText(interface.text('ImportDialog', 35 | 'Everything but already validated and partial strings')) 36 | self.rb_partial.setText(interface.text('ImportDialog', 'Partial strings')) 37 | self.rb_selection.setText(interface.text('ImportDialog', 'Selection only')) 38 | self.cb_replace.setText(interface.text('ImportDialog', 'Replace existing translation')) 39 | self.btn_import.setText(interface.text('ImportDialog', 'Import')) 40 | self.btn_cancel.setText(interface.text('ImportDialog', 'Cancel')) 41 | 42 | def keyPressEvent(self, event): 43 | if event.key() == Qt.Key.Key_Escape: 44 | self.close() 45 | else: 46 | super().keyPressEvent(event) 47 | 48 | def closeEvent(self, event): 49 | self.filename = None 50 | 51 | def translate(self): 52 | if not self.filename: 53 | return 54 | 55 | table = {} 56 | 57 | if self.filename.lower().endswith('.xml'): 58 | table = app_state.packages_storage.read_xml(self.filename) 59 | elif self.filename.lower().endswith('.stbl'): 60 | table = app_state.packages_storage.read_stbl(self.filename) 61 | elif self.filename.lower().endswith('.package'): 62 | table = app_state.packages_storage.read_package(self.filename) 63 | elif self.filename.lower().endswith('.json'): 64 | table = app_state.packages_storage.read_json(self.filename) 65 | elif self.filename.lower().endswith('.binary'): 66 | table = app_state.packages_storage.read_binary(self.filename) 67 | 68 | if self.rb_selection.isChecked(): 69 | items = app_state.tableview.selected_items() 70 | else: 71 | items = app_state.packages_storage.items() 72 | 73 | if not table or not items: 74 | return 75 | 76 | if self.rb_validated.isChecked(): 77 | flags = [FLAG_UNVALIDATED, FLAG_PROGRESS, FLAG_REPLACED] 78 | elif self.rb_validated_partial.isChecked(): 79 | flags = [FLAG_UNVALIDATED] 80 | elif self.rb_partial.isChecked(): 81 | flags = [FLAG_PROGRESS, FLAG_REPLACED] 82 | else: 83 | flags = [] 84 | 85 | progress_signals.initiate.emit(interface.text('System', 'Importing translate...'), len(items) / 100) 86 | 87 | for i, item in enumerate(items): 88 | if i % 100 == 0: 89 | progress_signals.increment.emit() 90 | 91 | if self.rb_all.isChecked() or self.rb_selection.isChecked() or item.flag in flags: 92 | sid = item.id 93 | if sid in table: 94 | source = item.source 95 | dest = item.translate 96 | translate = table[sid] 97 | if not compare(dest, translate) and not compare(source, translate): 98 | undo.wrap(item) 99 | if self.cb_replace.isChecked(): 100 | if item.flag != FLAG_UNVALIDATED: 101 | item.translate_old = item.translate 102 | item.translate = text_to_stbl(translate) 103 | item.flag = FLAG_VALIDATED 104 | else: 105 | if item.flag == FLAG_UNVALIDATED: 106 | item.translate = text_to_stbl(translate) 107 | item.flag = FLAG_VALIDATED 108 | else: 109 | item.translate_old = text_to_stbl(translate) 110 | 111 | undo.commit() 112 | 113 | color_signals.update.emit() 114 | progress_signals.finished.emit() 115 | 116 | def import_click(self): 117 | self.translate() 118 | self.close() 119 | 120 | def cancel_click(self): 121 | self.close() 122 | -------------------------------------------------------------------------------- /windows/replace_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtWidgets import QApplication, QDialog 6 | 7 | from .ui.replace_dialog import Ui_ReplaceDialog 8 | 9 | from singletons.interface import interface 10 | from singletons.signals import color_signals 11 | from singletons.state import app_state 12 | from singletons.undo import undo 13 | from utils.constants import * 14 | 15 | 16 | class ReplaceDialog(QDialog, Ui_ReplaceDialog): 17 | 18 | def __init__(self, parent=None): 19 | super().__init__(parent) 20 | self.setupUi(self) 21 | 22 | self.btn_replace.clicked.connect(self.replace_click) 23 | self.btn_cancel.clicked.connect(self.cancel_click) 24 | 25 | self.retranslate() 26 | 27 | def retranslate(self): 28 | self.setWindowTitle(interface.text('ReplaceDialog', 'Search and replace')) 29 | self.cb_case_sensitive.setText(interface.text('ReplaceDialog', 'Case sensitive')) 30 | self.label_search.setText(interface.text('ReplaceDialog', 'Search')) 31 | self.label_replace.setText(interface.text('ReplaceDialog', 'Replace')) 32 | self.groupbox.setTitle(interface.text('ReplaceDialog', 'Search and replace in:')) 33 | self.rb_all_strings.setText(interface.text('ReplaceDialog', 'All strings')) 34 | self.rb_not_validated_strings.setText(interface.text('ReplaceDialog', 'Not validated strings')) 35 | self.btn_cancel.setText(interface.text('ReplaceDialog', 'Cancel')) 36 | self.btn_replace.setText(interface.text('ReplaceDialog', 'OK')) 37 | 38 | def showEvent(self, event): 39 | self.cb_search.clearEditText() 40 | self.cb_replace.clearEditText() 41 | 42 | def keyPressEvent(self, event): 43 | if event.key() == Qt.Key.Key_Escape: 44 | self.close() 45 | else: 46 | super().keyPressEvent(event) 47 | 48 | def replace_click(self): 49 | search = self.cb_search.currentText() 50 | replace = self.cb_replace.currentText() 51 | 52 | if search: 53 | for item in app_state.packages_storage.items(): 54 | QApplication.processEvents() 55 | 56 | if self.rb_not_validated_strings.isChecked() and item.flag != FLAG_UNVALIDATED: 57 | continue 58 | 59 | flags = re.IGNORECASE if not self.cb_case_sensitive.isChecked() else 0 60 | if re.search(search, item.translate, flags=flags): 61 | undo.wrap(item) 62 | 63 | item.translate = re.sub(search, replace, item.translate, flags=flags) 64 | item.flag = FLAG_PROGRESS 65 | 66 | undo.commit() 67 | 68 | color_signals.update.emit() 69 | 70 | self.save_values() 71 | 72 | self.close() 73 | 74 | def cancel_click(self): 75 | self.close() 76 | 77 | @staticmethod 78 | def update_combobox(combobox): 79 | current_text = combobox.currentText() 80 | if current_text: 81 | values = [current_text] 82 | for i in range(combobox.count()): 83 | item_text = combobox.itemText(i) 84 | if len(values) < 10 and item_text not in values: 85 | values.append(item_text) 86 | combobox.clear() 87 | combobox.addItems(values) 88 | 89 | def save_values(self): 90 | self.update_combobox(self.cb_search) 91 | self.update_combobox(self.cb_replace) 92 | -------------------------------------------------------------------------------- /windows/translate_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import Qt, QObject, QThreadPool, QRunnable, Signal, Slot 4 | from PySide6.QtWidgets import QDialog 5 | from typing import Union, List 6 | 7 | from windows.ui.translate_dialog import Ui_TranslateDialog 8 | 9 | from storages.records import MainRecord 10 | 11 | import themes.light as light 12 | import themes.dark as dark 13 | 14 | from singletons.config import config 15 | from singletons.interface import interface 16 | from singletons.signals import progress_signals, color_signals 17 | from singletons.state import app_state 18 | from singletons.translator import translator 19 | from singletons.undo import undo 20 | from utils.functions import text_to_stbl, text_to_edit 21 | from utils.constants import * 22 | 23 | 24 | def split_by_char_limit(items: List[MainRecord], char_limit: int = 256) -> list: 25 | result = [] 26 | current_chunk = [] 27 | current_length = 0 28 | 29 | for item in items: 30 | text_length = len(item.source) 31 | if current_length + text_length > char_limit: 32 | result.append(current_chunk) 33 | current_chunk = [item] 34 | current_length = text_length 35 | else: 36 | current_chunk.append(item) 37 | current_length += text_length 38 | 39 | if current_chunk: 40 | result.append(current_chunk) 41 | 42 | return result 43 | 44 | 45 | class TranslateSignals(QObject): 46 | finished = Signal() 47 | warning = Signal(str) 48 | error = Signal(str) 49 | 50 | 51 | class BatchTranslateWorker(QRunnable): 52 | 53 | def __init__(self, chunk: Union[MainRecord, List[MainRecord]], engine: str): 54 | super().__init__() 55 | 56 | self.chunk = chunk 57 | self.engine = engine 58 | 59 | self.signals = TranslateSignals() 60 | 61 | def run(self): 62 | chunk = self.chunk 63 | is_fast = not isinstance(chunk, MainRecord) 64 | 65 | if is_fast: 66 | text_strings = [] 67 | 68 | for item in chunk: 69 | text_string = text_to_edit(item.source) 70 | hex_replacement_n = r"\x0a" 71 | hex_replacement_r = r"\x0d" 72 | text_string = text_string.replace("\n", hex_replacement_n) 73 | text_string = text_string.replace("\r", hex_replacement_r) 74 | text_strings.append(text_string) 75 | 76 | combined_text = '\n'.join(text_strings) 77 | 78 | else: 79 | combined_text = text_to_edit(chunk.source) 80 | 81 | response = translator.translate(self.engine, combined_text) 82 | 83 | if response.status_code == 200: 84 | translated_text = response.text 85 | 86 | if is_fast: 87 | translated_texts = translated_text.split('\n') 88 | 89 | if len(translated_texts) == len(chunk): 90 | for i, item in enumerate(chunk): 91 | undo.wrap(item) 92 | translated_text = translated_texts[i] 93 | hex_replacement_n = bytes(r"\x0a", 'utf-8').decode('unicode-escape') 94 | hex_replacement_r = bytes(r"\x0d", 'utf-8').decode('unicode-escape') 95 | translated_text = translated_text.replace("\\x0a", hex_replacement_n) 96 | translated_text = translated_text.replace("\\x0d", hex_replacement_r) 97 | item.translate = text_to_stbl(translated_text) 98 | item.flag = FLAG_VALIDATED 99 | else: 100 | self.signals.warning.emit(interface.text('TranslateDialog', 'Some lines could not be translated.')) 101 | 102 | else: 103 | undo.wrap(chunk) 104 | chunk.translate = text_to_stbl(translated_text) 105 | chunk.flag = FLAG_VALIDATED 106 | 107 | else: 108 | self.signals.error.emit(response.text) 109 | 110 | self.signals.finished.emit() 111 | 112 | 113 | class TranslateDialog(QDialog, Ui_TranslateDialog): 114 | 115 | def __init__(self, parent=None): 116 | super().__init__(parent) 117 | self.setupUi(self) 118 | 119 | self.cb_api.currentTextChanged.connect(self.change_api) 120 | 121 | self.btn_translate.clicked.connect(self.translate_click) 122 | self.btn_cancel.clicked.connect(self.cancel_click) 123 | 124 | self.__pool = QThreadPool() 125 | 126 | self.__progress = 0 127 | self.__translating = False 128 | self.__error = False 129 | self.__log = [] 130 | 131 | self.check_api() 132 | 133 | self.retranslate() 134 | 135 | def retranslate(self): 136 | self.setWindowTitle(interface.text('TranslateDialog', 'Batch translate')) 137 | self.rb_all.setText(interface.text('ImportDialog', 'Everything')) 138 | self.rb_validated.setText(interface.text('ImportDialog', 'Everything but already validated strings')) 139 | self.rb_validated_partial.setText(interface.text('ImportDialog', 140 | 'Everything but already validated and partial strings')) 141 | self.rb_partial.setText(interface.text('ImportDialog', 'Partial strings')) 142 | self.rb_selection.setText(interface.text('ImportDialog', 'Selection only')) 143 | self.btn_cancel.setText(interface.text('TranslateDialog', 'Cancel')) 144 | self.btn_translate.setText(interface.text('TranslateDialog', 'Translate')) 145 | self.rb_slow.setText(interface.text('TranslateDialog', 'Line-by-line translation')) 146 | self.rb_fast.setText(interface.text('TranslateDialog', 'Multiline translation')) 147 | self.lbl_slow.setText(interface.text('TranslateDialog', 'Slow but more accurate translation.')) 148 | self.lbl_fast.setText(interface.text('TranslateDialog', 'A faster, but perhaps less accurate translation.')) 149 | self.log_box.setTitle(interface.text('TranslateDialog', 'Log')) 150 | 151 | def showEvent(self, event): 152 | engine = config.value('api', 'engine') 153 | self.cb_api.clear() 154 | self.cb_api.addItems(translator.engines) 155 | engine_index = self.cb_api.findText(engine) 156 | self.cb_api.setCurrentIndex(engine_index if engine_index >= 0 else 0) 157 | 158 | def keyPressEvent(self, event): 159 | if event.key() == Qt.Key.Key_Escape: 160 | self.close() 161 | else: 162 | super().keyPressEvent(event) 163 | 164 | def change_api(self): 165 | config.set_value('api', 'engine', self.cb_api.currentText()) 166 | self.check_api() 167 | 168 | def check_api(self): 169 | api = self.cb_api.currentText().lower() 170 | if api == 'deepl': 171 | self.rb_fast.setEnabled(True) 172 | else: 173 | self.rb_fast.setEnabled(False) 174 | self.rb_slow.setChecked(True) 175 | 176 | def translate(self): 177 | progress_signals.initiate.emit(interface.text('System', 'Translating...'), 0) 178 | 179 | if self.rb_selection.isChecked(): 180 | items = app_state.tableview.selected_items() 181 | else: 182 | items = app_state.packages_storage.items() 183 | if self.rb_validated.isChecked(): 184 | items = [i for i in items if i.flag in (FLAG_UNVALIDATED, FLAG_PROGRESS, FLAG_REPLACED)] 185 | elif self.rb_validated_partial.isChecked(): 186 | items = [i for i in items if i.flag == FLAG_UNVALIDATED] 187 | elif self.rb_partial.isChecked(): 188 | items = [i for i in items if i.flag in (FLAG_PROGRESS, FLAG_REPLACED)] 189 | 190 | if not items: 191 | progress_signals.finished.emit() 192 | return 193 | 194 | self.btn_translate.setText(interface.text('TranslateDialog', 'Stop translate')) 195 | 196 | if self.rb_fast.isChecked(): 197 | chunk_items = split_by_char_limit(items, 1024) 198 | else: 199 | chunk_items = items 200 | 201 | self.__progress = len(chunk_items) 202 | self.__translating = True 203 | self.__error = False 204 | self.__log = [] 205 | 206 | self.edt_log.clear() 207 | 208 | progress_signals.initiate.emit(interface.text('System', 'Translating...'), self.__progress) 209 | 210 | for chunk in chunk_items: 211 | if not self.__error: 212 | worker = BatchTranslateWorker(chunk, self.cb_api.currentText()) 213 | worker.setAutoDelete(True) 214 | worker.signals.warning.connect(self.__warning_translate_chunk) 215 | worker.signals.error.connect(self.__error_translate_chunk) 216 | worker.signals.finished.connect(self.__finished_translate_chunk) 217 | self.__pool.start(worker) 218 | 219 | def stop_translate(self): 220 | self.__progress = self.__pool.activeThreadCount() 221 | self.__pool.clear() 222 | progress_signals.initiate.emit(interface.text('System', 223 | 'Stopping translate, waiting for the finish of the threads...'), 224 | self.__progress) 225 | self.__pool.waitForDone() 226 | 227 | @Slot() 228 | def __finished_translate_chunk(self): 229 | self.__progress -= 1 230 | if self.__progress == 0: 231 | undo.commit() 232 | self.__progress = 0 233 | self.__translating = False 234 | self.btn_translate.setText(interface.text('TranslateDialog', 'Translate')) 235 | color_signals.update.emit() 236 | progress_signals.finished.emit() 237 | 238 | app_state.tableview.refresh() 239 | progress_signals.increment.emit() 240 | 241 | @Slot(str) 242 | def __error_translate_chunk(self, text: str): 243 | if not self.__error: 244 | self.__error = True 245 | color = dark.TEXT_ERROR if config.value('interface', 'theme') == 'dark' else light.TEXT_ERROR 246 | self.__log.append(f'{text}') 247 | self.print_log() 248 | self.stop_translate() 249 | 250 | @Slot(str) 251 | def __warning_translate_chunk(self, text: str): 252 | self.__log.append(text) 253 | self.print_log() 254 | 255 | def print_log(self): 256 | self.edt_log.setText('
'.join(self.__log)) 257 | self.edt_log.verticalScrollBar().setValue(self.edt_log.verticalScrollBar().maximum()) 258 | 259 | def translate_click(self): 260 | if not self.__translating: 261 | self.translate() 262 | else: 263 | self.stop_translate() 264 | 265 | def cancel_click(self): 266 | self.close() 267 | -------------------------------------------------------------------------------- /windows/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voky1/sims4-translator/606ca092b97b9ecd964363dfa38998d2558c3a01/windows/ui/__init__.py -------------------------------------------------------------------------------- /windows/ui/edit_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject, Qt 4 | from PySide6.QtWidgets import QComboBox, QLabel, QLineEdit, QPushButton, QSplitter, QVBoxLayout, QHBoxLayout, QWidget 5 | from PySide6.QtGui import QIcon 6 | 7 | from widgets.tableview import QDictionaryTableView 8 | from widgets.editor import QTextEditor 9 | 10 | 11 | class Ui_EditDialog(object): 12 | 13 | def setupUi(self, EditDialog): 14 | EditDialog.resize(1009, 663) 15 | EditDialog.setMinimumSize(961, 611) 16 | 17 | self.lbl_original = QLabel(EditDialog) 18 | self.lbl_original_diff = QLabel(EditDialog) 19 | self.lbl_translate = QLabel(EditDialog) 20 | self.lbl_translate_diff = QLabel(EditDialog) 21 | 22 | self.txt_original = QTextEditor() 23 | self.txt_original.setReadOnly(True) 24 | 25 | self.txt_original_diff = QTextEditor() 26 | self.txt_original_diff.setReadOnly(True) 27 | 28 | self.txt_translate = QTextEditor() 29 | 30 | self.txt_translate_diff = QTextEditor() 31 | self.txt_translate_diff.setReadOnly(True) 32 | 33 | self.txt_search = QTextEditor() 34 | self.txt_search.setReadOnly(True) 35 | 36 | self.txt_resource = QLineEdit(EditDialog) 37 | self.txt_resource.setReadOnly(True) 38 | self.txt_resource.setObjectName('monospace') 39 | 40 | self.tableview = QDictionaryTableView(EditDialog) 41 | 42 | layout = QVBoxLayout(EditDialog) 43 | layout.setSpacing(8) 44 | 45 | layout.addWidget(self.txt_resource) 46 | 47 | left_widget = QWidget(EditDialog) 48 | right_widget = QWidget(EditDialog) 49 | 50 | left_layout = QVBoxLayout(left_widget) 51 | left_layout.setContentsMargins(0, 0, 0, 0) 52 | left_layout.addWidget(self.lbl_original) 53 | left_layout.addWidget(self.txt_original) 54 | left_layout.addWidget(self.lbl_original_diff) 55 | left_layout.addWidget(self.txt_original_diff) 56 | 57 | right_layout = QVBoxLayout(right_widget) 58 | right_layout.setContentsMargins(0, 0, 0, 0) 59 | right_layout.addWidget(self.lbl_translate) 60 | right_layout.addWidget(self.txt_translate) 61 | right_layout.addWidget(self.lbl_translate_diff) 62 | right_layout.addWidget(self.txt_translate_diff) 63 | 64 | top_splitter = QSplitter(Qt.Orientation.Horizontal) 65 | top_splitter.addWidget(self.tableview) 66 | top_splitter.addWidget(self.txt_search) 67 | top_splitter.setSizes([500, 300]) 68 | top_splitter.setHandleWidth(8) 69 | 70 | bottom_splitter = QSplitter(Qt.Orientation.Horizontal) 71 | bottom_splitter.addWidget(left_widget) 72 | bottom_splitter.addWidget(right_widget) 73 | bottom_splitter.setSizes([300, 500]) 74 | bottom_splitter.setHandleWidth(8) 75 | 76 | splitter = QSplitter(Qt.Orientation.Vertical) 77 | splitter.addWidget(top_splitter) 78 | splitter.addWidget(bottom_splitter) 79 | splitter.setSizes([200, 350]) 80 | splitter.setHandleWidth(8) 81 | 82 | layout.addWidget(splitter) 83 | 84 | self.txt_comment = QLineEdit(EditDialog) 85 | 86 | layout.addWidget(self.txt_comment) 87 | 88 | self.cb_api = QComboBox(EditDialog) 89 | 90 | self.btn_translate = QPushButton(EditDialog) 91 | self.btn_translate.setIcon(QIcon(':/images/api.png')) 92 | self.btn_translate.setAutoDefault(False) 93 | 94 | self.lbl_status = QLabel(EditDialog) 95 | 96 | self.btn_ok = QPushButton(EditDialog) 97 | self.btn_cancel = QPushButton(EditDialog) 98 | 99 | self.btn_ok.setDefault(True) 100 | self.btn_cancel.setAutoDefault(False) 101 | 102 | hlayout = QHBoxLayout() 103 | 104 | hlayout.addWidget(self.cb_api) 105 | hlayout.addWidget(self.btn_translate) 106 | hlayout.addWidget(self.lbl_status) 107 | hlayout.addStretch() 108 | hlayout.addWidget(self.btn_cancel) 109 | hlayout.addWidget(self.btn_ok) 110 | 111 | layout.addLayout(hlayout) 112 | 113 | QMetaObject.connectSlotsByName(EditDialog) 114 | -------------------------------------------------------------------------------- /windows/ui/export_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject 4 | from PySide6.QtWidgets import QCheckBox, QGroupBox, QHBoxLayout, QPushButton, QRadioButton, QVBoxLayout 5 | 6 | 7 | class Ui_ExportDialog(object): 8 | 9 | def setupUi(self, ExportDialog): 10 | ExportDialog.resize(390, 134) 11 | ExportDialog.setMinimumSize(390, 134) 12 | 13 | layout = QVBoxLayout(ExportDialog) 14 | 15 | self.gb_rec = QGroupBox(ExportDialog) 16 | 17 | layout_rec = QVBoxLayout(self.gb_rec) 18 | 19 | self.rb_all = QRadioButton(self.gb_rec) 20 | 21 | self.rb_translated = QRadioButton(self.gb_rec) 22 | self.rb_translated.setChecked(True) 23 | 24 | self.rb_selection = QRadioButton(self.gb_rec) 25 | 26 | layout_rec.addWidget(self.rb_all) 27 | layout_rec.addWidget(self.rb_translated) 28 | layout_rec.addWidget(self.rb_selection) 29 | 30 | layout.addWidget(self.gb_rec) 31 | 32 | self.cb_current_instance = QCheckBox(ExportDialog) 33 | self.cb_separate_instances = QCheckBox(ExportDialog) 34 | self.cb_separate_packages = QCheckBox(ExportDialog) 35 | 36 | layout.addWidget(self.cb_current_instance) 37 | layout.addWidget(self.cb_separate_instances) 38 | layout.addWidget(self.cb_separate_packages) 39 | layout.addStretch() 40 | 41 | layout_buttons = QHBoxLayout() 42 | layout_buttons.setContentsMargins(0, 4, 0, 0) 43 | 44 | self.btn_export = QPushButton(ExportDialog) 45 | self.btn_cancel = QPushButton(ExportDialog) 46 | 47 | self.btn_export.setDefault(True) 48 | self.btn_cancel.setAutoDefault(False) 49 | 50 | layout_buttons.addStretch() 51 | layout_buttons.addWidget(self.btn_cancel) 52 | layout_buttons.addWidget(self.btn_export) 53 | 54 | layout.addLayout(layout_buttons) 55 | 56 | QMetaObject.connectSlotsByName(ExportDialog) 57 | -------------------------------------------------------------------------------- /windows/ui/import_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject 4 | from PySide6.QtWidgets import QCheckBox, QGroupBox, QHBoxLayout, QPushButton, QRadioButton, QVBoxLayout 5 | 6 | 7 | class Ui_ImportDialog(object): 8 | 9 | def setupUi(self, ImportDialog): 10 | ImportDialog.setMinimumSize(470, 0) 11 | 12 | layout = QVBoxLayout(ImportDialog) 13 | 14 | self.gb_overwrite = QGroupBox(ImportDialog) 15 | 16 | layout_over = QVBoxLayout(self.gb_overwrite) 17 | 18 | self.rb_all = QRadioButton(self.gb_overwrite) 19 | self.rb_validated = QRadioButton(self.gb_overwrite) 20 | self.rb_validated_partial = QRadioButton(self.gb_overwrite) 21 | self.rb_partial = QRadioButton(self.gb_overwrite) 22 | self.rb_selection = QRadioButton(self.gb_overwrite) 23 | 24 | self.rb_validated.setChecked(True) 25 | 26 | layout_over.addWidget(self.rb_all) 27 | layout_over.addWidget(self.rb_validated) 28 | layout_over.addWidget(self.rb_validated_partial) 29 | layout_over.addWidget(self.rb_partial) 30 | layout_over.addWidget(self.rb_selection) 31 | 32 | layout.addWidget(self.gb_overwrite) 33 | layout.addStretch() 34 | 35 | self.cb_replace = QCheckBox(ImportDialog) 36 | self.cb_replace.setChecked(True) 37 | 38 | self.btn_import = QPushButton(ImportDialog) 39 | self.btn_cancel = QPushButton(ImportDialog) 40 | 41 | self.btn_import.setDefault(True) 42 | self.btn_cancel.setAutoDefault(False) 43 | 44 | layout_buttons = QHBoxLayout() 45 | layout_buttons.setContentsMargins(0, 4, 0, 0) 46 | 47 | layout_buttons.addWidget(self.cb_replace) 48 | layout_buttons.addStretch() 49 | layout_buttons.addWidget(self.btn_cancel) 50 | layout_buttons.addWidget(self.btn_import) 51 | 52 | layout.addLayout(layout_buttons) 53 | 54 | ImportDialog.adjustSize() 55 | ImportDialog.setMinimumSize(ImportDialog.size()) 56 | 57 | QMetaObject.connectSlotsByName(ImportDialog) 58 | -------------------------------------------------------------------------------- /windows/ui/options_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject, Qt 4 | from PySide6.QtWidgets import QWidget, QAbstractItemView, QCheckBox, QComboBox, QGroupBox, QHBoxLayout, QLabel, \ 5 | QLineEdit, QPushButton, QTableView, QVBoxLayout, QTabWidget, QHeaderView 6 | 7 | 8 | class Ui_OptionsDialog(object): 9 | 10 | def setupUi(self, OptionsDialog): 11 | OptionsDialog.resize(545, 490) 12 | OptionsDialog.setMinimumSize(545, 490) 13 | 14 | layout = QVBoxLayout(OptionsDialog) 15 | 16 | self.tab_general = QWidget() 17 | self.tab_dictionaries = QWidget() 18 | 19 | self.tabs = QTabWidget(OptionsDialog) 20 | self.tabs.addTab(self.tab_general, '') 21 | self.tabs.addTab(self.tab_dictionaries, '') 22 | 23 | layout.addWidget(self.tabs) 24 | 25 | self.build_general_tab() 26 | self.build_dictionaries_tab() 27 | 28 | QMetaObject.connectSlotsByName(OptionsDialog) 29 | 30 | def build_general_tab(self): 31 | vlayout = QVBoxLayout(self.tab_general) 32 | 33 | self.gb_interface = QGroupBox(self.tab_general) 34 | 35 | layout_group = QVBoxLayout(self.gb_interface) 36 | 37 | layout_lang = QHBoxLayout() 38 | layout_theme = QHBoxLayout() 39 | 40 | self.lbl_language = QLabel(self.gb_interface) 41 | self.lbl_language_authors = QLabel(self.gb_interface) 42 | self.lbl_language_hint = QLabel(self.gb_interface) 43 | self.cb_language = QComboBox(self.gb_interface) 44 | 45 | self.lbl_theme = QLabel(self.gb_interface) 46 | self.lbl_theme_hint = QLabel(self.gb_interface) 47 | self.cb_theme = QComboBox(self.gb_interface) 48 | 49 | self.lbl_language_hint.setWordWrap(True) 50 | self.lbl_language_hint.setObjectName('muted') 51 | 52 | self.lbl_language.setMinimumHeight(26) 53 | self.lbl_theme.setMinimumHeight(26) 54 | 55 | self.lbl_theme_hint.setVisible(False) 56 | self.lbl_theme_hint.setWordWrap(True) 57 | 58 | layout_lang_lbl = QVBoxLayout() 59 | layout_lang_authors = QHBoxLayout() 60 | layout_lang_hint = QVBoxLayout() 61 | 62 | layout_theme_lbl = QVBoxLayout() 63 | layout_theme_plug = QHBoxLayout() 64 | layout_theme_hint = QVBoxLayout() 65 | 66 | layout_lang_authors.addWidget(self.cb_language) 67 | layout_lang_authors.addWidget(self.lbl_language_authors) 68 | layout_lang_authors.addStretch() 69 | 70 | layout_lang_lbl.addWidget(self.lbl_language) 71 | layout_lang_lbl.addStretch() 72 | 73 | layout_lang_hint.addLayout(layout_lang_authors) 74 | layout_lang_hint.addWidget(self.lbl_language_hint) 75 | 76 | layout_lang.addLayout(layout_lang_lbl) 77 | layout_lang.addLayout(layout_lang_hint) 78 | 79 | layout_theme_lbl.addWidget(self.lbl_theme) 80 | layout_theme_lbl.addStretch() 81 | 82 | layout_theme_plug.addWidget(self.cb_theme) 83 | layout_theme_plug.addStretch() 84 | 85 | layout_theme_hint.addLayout(layout_theme_plug) 86 | layout_theme_hint.addWidget(self.lbl_theme_hint) 87 | 88 | layout_theme.addLayout(layout_theme_lbl) 89 | layout_theme.addLayout(layout_theme_hint) 90 | 91 | self.lbl_language.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) 92 | self.lbl_theme.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) 93 | 94 | self.lbl_language.setMinimumWidth(75) 95 | self.lbl_theme.setMinimumWidth(75) 96 | 97 | layout_group.addLayout(layout_lang) 98 | layout_group.addLayout(layout_theme) 99 | 100 | vlayout.addWidget(self.gb_interface) 101 | 102 | gbox = QGroupBox(self.tab_general) 103 | layout_group = QVBoxLayout(gbox) 104 | 105 | self.cb_backup = QCheckBox(gbox) 106 | self.cb_experemental = QCheckBox(gbox) 107 | self.cb_strong = QCheckBox(gbox) 108 | 109 | layout_group.addWidget(self.cb_backup) 110 | layout_group.addWidget(self.cb_experemental) 111 | layout_group.addWidget(self.cb_strong) 112 | 113 | vlayout.addWidget(gbox) 114 | 115 | self.gb_lang = QGroupBox(self.tab_general) 116 | 117 | layout_lang = QHBoxLayout(self.gb_lang) 118 | 119 | self.label_source = QLabel(self.gb_lang) 120 | self.label_dest = QLabel(self.gb_lang) 121 | 122 | self.cb_source = QComboBox(self.gb_lang) 123 | self.cb_dest = QComboBox(self.gb_lang) 124 | 125 | layout_lang.addStretch() 126 | layout_lang.addWidget(self.label_source) 127 | layout_lang.addWidget(self.cb_source) 128 | layout_lang.addStretch() 129 | layout_lang.addWidget(self.label_dest) 130 | layout_lang.addWidget(self.cb_dest) 131 | layout_lang.addStretch() 132 | 133 | vlayout.addWidget(self.gb_lang) 134 | 135 | self.gb_deepl = QGroupBox(self.tab_general) 136 | 137 | layout_deepl = QHBoxLayout(self.gb_deepl) 138 | 139 | self.txt_deepl_key = QLineEdit(self.gb_deepl) 140 | 141 | layout_deepl.addWidget(self.txt_deepl_key) 142 | 143 | vlayout.addWidget(self.gb_deepl) 144 | 145 | vlayout.addStretch() 146 | 147 | def build_dictionaries_tab(self): 148 | vlayout = QVBoxLayout(self.tab_dictionaries) 149 | vlayout.setSpacing(8) 150 | 151 | self.gb_path = QGroupBox(self.tab_dictionaries) 152 | 153 | layout_path = QHBoxLayout(self.gb_path) 154 | 155 | self.txt_path = QLineEdit(self.gb_path) 156 | 157 | self.btn_path = QPushButton(self.gb_path) 158 | self.btn_path.setText('...') 159 | self.btn_path.setAutoDefault(False) 160 | 161 | layout_path.addWidget(self.txt_path) 162 | layout_path.addWidget(self.btn_path) 163 | 164 | vlayout.addWidget(self.gb_path) 165 | 166 | self.tableview = QTableView(self.tab_dictionaries) 167 | self.tableview.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 168 | self.tableview.setAutoScroll(False) 169 | self.tableview.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) 170 | self.tableview.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) 171 | self.tableview.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 172 | self.tableview.setShowGrid(False) 173 | self.tableview.setGridStyle(Qt.PenStyle.NoPen) 174 | self.tableview.setWordWrap(False) 175 | self.tableview.horizontalHeader().setVisible(False) 176 | 177 | header = self.tableview.verticalHeader() 178 | header.setSectionResizeMode(QHeaderView.ResizeMode.Fixed) 179 | header.setDefaultSectionSize(26) 180 | header.setVisible(False) 181 | 182 | self.btn_build = QPushButton(self.tab_dictionaries) 183 | self.btn_build.setAutoDefault(False) 184 | 185 | vlayout.addWidget(self.tableview) 186 | vlayout.addWidget(self.btn_build) 187 | -------------------------------------------------------------------------------- /windows/ui/replace_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject 4 | from PySide6.QtWidgets import QCheckBox, QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QPushButton, \ 5 | QRadioButton, QVBoxLayout 6 | 7 | 8 | class Ui_ReplaceDialog(object): 9 | 10 | def setupUi(self, ReplaceDialog): 11 | ReplaceDialog.setMinimumSize(450, 0) 12 | 13 | layout = QVBoxLayout(ReplaceDialog) 14 | 15 | layout_form = QFormLayout() 16 | 17 | self.label_search = QLabel(ReplaceDialog) 18 | self.label_replace = QLabel(ReplaceDialog) 19 | 20 | layout_form.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label_search) 21 | layout_form.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_replace) 22 | 23 | self.cb_search = QComboBox(ReplaceDialog) 24 | self.cb_search.setEditable(True) 25 | 26 | self.cb_replace = QComboBox(ReplaceDialog) 27 | self.cb_replace.setEditable(True) 28 | 29 | layout_form.setWidget(0, QFormLayout.ItemRole.FieldRole, self.cb_search) 30 | layout_form.setWidget(1, QFormLayout.ItemRole.FieldRole, self.cb_replace) 31 | 32 | layout.addLayout(layout_form) 33 | 34 | self.groupbox = QGroupBox(ReplaceDialog) 35 | 36 | layout_group = QVBoxLayout(self.groupbox) 37 | 38 | self.rb_all_strings = QRadioButton(self.groupbox) 39 | 40 | self.rb_not_validated_strings = QRadioButton(self.groupbox) 41 | self.rb_not_validated_strings.setChecked(True) 42 | 43 | layout_group.addWidget(self.rb_all_strings) 44 | layout_group.addWidget(self.rb_not_validated_strings) 45 | 46 | layout.addWidget(self.groupbox) 47 | layout.addStretch() 48 | 49 | layout_buttons = QHBoxLayout() 50 | layout_buttons.setContentsMargins(0, 4, 0, 0) 51 | 52 | self.cb_case_sensitive = QCheckBox(ReplaceDialog) 53 | self.cb_case_sensitive.setChecked(True) 54 | 55 | self.btn_replace = QPushButton(ReplaceDialog) 56 | self.btn_cancel = QPushButton(ReplaceDialog) 57 | 58 | self.btn_replace.setDefault(True) 59 | self.btn_cancel.setAutoDefault(False) 60 | 61 | layout_buttons.addWidget(self.cb_case_sensitive) 62 | layout_buttons.addStretch() 63 | layout_buttons.addWidget(self.btn_cancel) 64 | layout_buttons.addWidget(self.btn_replace) 65 | 66 | layout.addLayout(layout_buttons) 67 | 68 | ReplaceDialog.adjustSize() 69 | ReplaceDialog.setMinimumSize(ReplaceDialog.size()) 70 | 71 | QMetaObject.connectSlotsByName(ReplaceDialog) 72 | -------------------------------------------------------------------------------- /windows/ui/translate_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from PySide6.QtCore import QMetaObject 4 | from PySide6.QtWidgets import QComboBox, QGroupBox, QHBoxLayout, QPushButton, QRadioButton, QVBoxLayout, \ 5 | QLabel, QTextEdit 6 | 7 | 8 | class Ui_TranslateDialog(object): 9 | 10 | def setupUi(self, TranslateDialog): 11 | TranslateDialog.resize(590, 470) 12 | TranslateDialog.setMinimumSize(590, 470) 13 | 14 | layout = QVBoxLayout(TranslateDialog) 15 | 16 | gbox = QGroupBox(TranslateDialog) 17 | vlayout = QVBoxLayout(gbox) 18 | 19 | self.rb_all = QRadioButton(gbox) 20 | self.rb_validated = QRadioButton(gbox) 21 | self.rb_validated_partial = QRadioButton(gbox) 22 | self.rb_partial = QRadioButton(gbox) 23 | self.rb_selection = QRadioButton(gbox) 24 | 25 | self.rb_validated.setChecked(True) 26 | 27 | vlayout.addWidget(self.rb_all) 28 | vlayout.addWidget(self.rb_validated) 29 | vlayout.addWidget(self.rb_validated_partial) 30 | vlayout.addWidget(self.rb_partial) 31 | vlayout.addWidget(self.rb_selection) 32 | 33 | layout.addWidget(gbox) 34 | 35 | gbox2 = QGroupBox(TranslateDialog) 36 | vlayout2 = QVBoxLayout(gbox2) 37 | 38 | self.rb_slow = QRadioButton(gbox2) 39 | self.rb_fast = QRadioButton(gbox2) 40 | 41 | self.lbl_slow = QLabel(gbox2) 42 | self.lbl_fast = QLabel(gbox2) 43 | 44 | self.rb_slow.setStyleSheet('margin-bottom: 0;') 45 | self.rb_fast.setStyleSheet('margin-bottom: 0;') 46 | self.lbl_slow.setStyleSheet('margin-bottom: 6px;') 47 | 48 | self.lbl_slow.setWordWrap(True) 49 | self.lbl_fast.setWordWrap(True) 50 | 51 | self.lbl_slow.setObjectName('muted') 52 | self.lbl_fast.setObjectName('muted') 53 | 54 | self.rb_slow.setChecked(True) 55 | 56 | vlayout2.addWidget(self.rb_slow) 57 | vlayout2.addWidget(self.lbl_slow) 58 | vlayout2.addWidget(self.rb_fast) 59 | vlayout2.addWidget(self.lbl_fast) 60 | 61 | layout.addWidget(gbox2) 62 | 63 | self.log_box = QGroupBox(TranslateDialog) 64 | vlayout3 = QVBoxLayout(self.log_box) 65 | 66 | self.edt_log = QTextEdit(self.log_box) 67 | self.edt_log.setReadOnly(True) 68 | 69 | vlayout3.addWidget(self.edt_log) 70 | 71 | layout.addWidget(self.log_box) 72 | 73 | hlayout = QHBoxLayout() 74 | hlayout.setContentsMargins(0, 4, 0, 0) 75 | 76 | self.cb_api = QComboBox(TranslateDialog) 77 | 78 | self.btn_translate = QPushButton(TranslateDialog) 79 | self.btn_cancel = QPushButton(TranslateDialog) 80 | 81 | self.btn_translate.setDefault(True) 82 | self.btn_cancel.setAutoDefault(False) 83 | 84 | hlayout.addWidget(self.cb_api) 85 | hlayout.addStretch() 86 | hlayout.addWidget(self.btn_cancel) 87 | hlayout.addWidget(self.btn_translate) 88 | 89 | layout.addLayout(hlayout) 90 | 91 | QMetaObject.connectSlotsByName(TranslateDialog) 92 | --------------------------------------------------------------------------------