From a863cebc661e01869863deeb4ccd201d133fd4bf Mon Sep 17 00:00:00 2001 From: aussig Date: Mon, 1 Apr 2024 07:48:15 +0100 Subject: [PATCH 01/33] [2188] Add optional lang parameter to l10n.Translations.translate() to allow language to be overridden. --- PLUGINS.md | 103 +++++++++++++++++++++++++++++------------------------ l10n.py | 39 +++++++++++++++----- 2 files changed, 86 insertions(+), 56 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 3048f345..5800efb0 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -31,7 +31,7 @@ then you'll need to be using an appropriate version of Python. The current version is listed in the [Environment section of Releasing.md](https://github.com/EDCD/EDMarketConnector/blob/main/docs/Releasing.md#environment). If you're developing your plugin simply against an install of EDMarketConnector -then you'll be relying on the bundled version of Python (it's baked +then you'll be relying on the bundled version of Python (it's baked into the .exe via the py2exe build process). Please be sure to read the [Avoiding potential pitfalls](#avoiding-potential-pitfalls) @@ -40,21 +40,21 @@ EDMarketConnector code including whole application crashes. ## Being aware of core application changes -It is highly advisable to ensure you are aware of all EDMarketConnector -releases, including the pre-releases. The -beta and -rc changelogs will +It is highly advisable to ensure you are aware of all EDMarketConnector +releases, including the pre-releases. The -beta and -rc changelogs will contain valuable information about any forthcoming changes that affect plugins. The easiest way is: 1. Login to [GitHub](https://github.com). 2. Navigate to [EDMarketConnector](https://github.com/EDCD/EDMarketConnector). - 3. Click the 'Watch' (or 'Unwatch' if you previously set up any watches on - us). It's currently (2021-05-13) the left-most button of 3 near the + 3. Click the 'Watch' (or 'Unwatch' if you previously set up any watches on + us). It's currently (2021-05-13) the left-most button of 3 near the top-right of the page. 4. Click 'Custom'. 5. Ensure 'Releases' is selected. 6. Click 'Apply'. -And, of course, either ensure you check your GitHub messages regularly, or +And, of course, either ensure you check your GitHub messages regularly, or have it set up to email you such notifications. You should also keep an eye on [our GitHub Discussions](https://github.com/EDCD/EDMarketConnector/discussions) @@ -113,13 +113,13 @@ from the original files unless specified as allowed in this section. Use `monitor.game_running()` as follows in case a plugin needs to know if we think the game is running. *NB: This is a function, and should be called as such. Using the bare word `game_running` will always be `True`.* - + ``` from monitor import monitor ... if monitor.game_running(): ... -``` +``` Use `monitor.is_live_galaxy()` to determine if the player is playing in the Live galaxy. Note the implementation details of this. At time of writing it @@ -135,7 +135,7 @@ append a string to call out your plugin if you wish). `from ttkHyperlinkLabel import HyperlinkLabel` and `import myNotebook as nb` - For creating UI elements. -In addition to the above we also explicitly package the following python +In addition to the above we also explicitly package the following python modules for plugin use: - shutil @@ -252,7 +252,7 @@ include variables, and even the returns of functions, in the output. ## Checking core EDMC version -If you have code that needs to act differently under different versions of +If you have code that needs to act differently under different versions of this application then you can check utilise `config.appversion`. Prior to version 5.0.0 this was a simple string. From 5.0.0 onwards it is, @@ -313,7 +313,7 @@ Mac, and `$TMP/EDMarketConnector.log` on Linux. ## Avoiding potential pitfalls -There are a number of things that your code should either do or avoiding +There are a number of things that your code should either do or avoiding doing so as to play nicely with the core EDMarketConnector code and not risk causing application crashes or hangs. @@ -324,12 +324,12 @@ See the section on [packaging extra modules](#your-plugin-directory-name-must-be ### Use a thread for long-running code -By default, your plugin code will be running in the main thread. So, if you -perform some operation that takes significant time (more than a second) you -will be blocking both the core code from continuing *and* any other plugins +By default, your plugin code will be running in the main thread. So, if you +perform some operation that takes significant time (more than a second) you +will be blocking both the core code from continuing *and* any other plugins from running their main-thread code. -This includes any connections to remote services, such as a website or +This includes any connections to remote services, such as a website or remote database. So please place such code within its own thread. See the [EDSM plugin](https://github.com/EDCD/EDMarketConnector/blob/main/plugins/edsm.py) @@ -338,20 +338,20 @@ with a queue to send data, and telling the sub-thread to stop during shutdown. ### All tkinter calls in main thread -The only tkinter calls that should ever be made from a sub-thread are +The only tkinter calls that should ever be made from a sub-thread are `event_generate()` calls to send data back to the main thread. -Any attempt to manipulate tkinter UI elements directly from a sub-thread +Any attempt to manipulate tkinter UI elements directly from a sub-thread will most likely crash the whole program. See the [EDSM plugin](https://github.com/EDCD/EDMarketConnector/blob/main/plugins/edsm.py) -code for an example of using `event_generate()` to cause the plugin main -thread code to update a UI element. Start from the `plugin_app()` +code for an example of using `event_generate()` to cause the plugin main +thread code to update a UI element. Start from the `plugin_app()` implementation. ### Do not call tkinter `event_generate` during shutdown. -However, you must **not** make *any* tkinter `event_generate()` call whilst +However, you must **not** make *any* tkinter `event_generate()` call whilst the application is shutting down. The application shutdown sequence is itself triggered from the `<>` event @@ -359,8 +359,8 @@ handler, and generating another event from any code in, or called from, there causes the application to hang somewhere in the tk libraries. You can detect if the application is shutting down with the boolean -`config.shutting_down`. Note that although this is technically a function -its implementation is of a property on `config.AbstractConfig` and thus you +`config.shutting_down`. Note that although this is technically a function +its implementation is of a property on `config.AbstractConfig` and thus you should treat it as a variable. **Do NOT use**: @@ -372,7 +372,7 @@ should treat it as a variable. # During shutdown ``` -as this will cause the 'During shutdown' branch to *always* be taken, as in +as this will cause the 'During shutdown' branch to *always* be taken, as in this context you're testing if the function exists, and that is always True. So instead use: @@ -417,8 +417,8 @@ your plugin's settings in a platform-independent way. Previously this was done with a single set and two get methods, the new methods provide better type safety. -If you want to maintain compatibility with pre-5.0.0 versions of this -application (please encourage plugin users to update!) then you'll need to +If you want to maintain compatibility with pre-5.0.0 versions of this +application (please encourage plugin users to update!) then you'll need to include this code in at least once in your plugin (no harm in putting it in all modules/files): @@ -699,8 +699,8 @@ cause `state['NavRoute'] = None`, but if you open the galaxy map in-game and cause an automatic re-plot of last route, then a new `NavRoute` event will also be generated and passed to plugins. -[2] - Some data from the CAPI is sometimes returned as a `list` (when all -members are present) and other times as an integer-keyed `dict` (when at +[2] - Some data from the CAPI is sometimes returned as a `list` (when all +members are present) and other times as an integer-keyed `dict` (when at least one member is missing, so the indices are not contiguous). We choose to always convert to the integer-keyed `dict` form so that code utilising the data is simpler. @@ -751,7 +751,7 @@ Journal `ModuleInfo` event. `OnFoot` is an indication as to if the player is on-foot, rather than in a vehicle. -`Component`, `Item`, `Consumable` & `Data` are `dict`s tracking your +`Component`, `Item`, `Consumable` & `Data` are `dict`s tracking your Odyssey MicroResources in your Ship Locker. `BacKPack` contains `dict`s for the same when you're on-foot. @@ -760,10 +760,10 @@ relating to suits and their loadouts. New in version 5.0.1: -`Odyssey` boolean based on the presence of such a flag in the `LoadGame` +`Odyssey` boolean based on the presence of such a flag in the `LoadGame` event. Defaults to `False`, i.e. if no such key in the event. -The previously undocumented `Horizons` boolean is similarly from `LoadGame`, +The previously undocumented `Horizons` boolean is similarly from `LoadGame`, but blindly retrieves the value rather than having a strict default. There'd be an exception if it wasn't there, and the value would be `None`. Note that this is **NOT** the same as the return from @@ -821,7 +821,7 @@ if that's what was in the file. New in version 5.8.0: -`StarPos`, `SystemAddress`, `SystemName` and `SystemPopulation` have been +`StarPos`, `SystemAddress`, `SystemName` and `SystemPopulation` have been added to the `state` dictionary. Best efforts data pertaining to the star system the player is in. @@ -853,8 +853,8 @@ react to either in your plugin code then either compare in a case insensitive manner or check for both. The difference in case allows you to differentiate between the two scenarios. -**NB: Any of these events are passing to `journal_entry_cqc` rather than to -`journal_entry` if player has loaded into Arena (CQC).** +**NB: Any of these events are passing to `journal_entry_cqc` rather than to +`journal_entry` if player has loaded into Arena (CQC).** This event is not sent when EDMarketConnector is running on a different machine so you should not *rely* on receiving this event. @@ -871,15 +871,15 @@ Examples of this are: 1. Every `NavRoute` event contains the full `Route` array as loaded from `NavRoute.json`. - + *NB: There is no indication available when a player cancels a route.* The game itself does not provide any such, not in a Journal event, not in a `Status.json` flag. - + The Journal documentation v28 is incorrect about the event and file being `Route(.json)` the word is `NavRoute`. Also the format of the data is, e.g. - + ```json { "timestamp":"2021-03-10T11:31:37Z", "event":"NavRoute", @@ -893,9 +893,9 @@ Examples of this are: ``` 1. Every `ModuleInfo` event contains the full data as loaded from the - `ModulesInfo.json` file. Note that we use the singular form here to + `ModulesInfo.json` file. Note that we use the singular form here to stay consistent with the Journal event name. - + --- ### Journal entry in CQC @@ -955,7 +955,7 @@ def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]): sys.stderr.write("Hardpoints {}\n".format(is_deployed and "deployed" or "stowed")) ``` -`dashboard_entry()` is called with the latest data from the `Status.json` +`dashboard_entry()` is called with the latest data from the `Status.json` file when an update to that file is detected. This will be when something on the player's cockpit display changes - @@ -1208,6 +1208,15 @@ Wrap each string that needs translating with the `_()` function, e.g.: somewidget["text"] = _("Happy!") ``` +If you wish to override EDMCs current language when translating, +`l10n.Translations.translate()` takes an optional `lang` parameter which can +be passed a language identifier. For example to override all translations +to German: + +```python +_ = functools.partial(l10n.Translations.translate, context=__file__, lang="de") +``` + If you display localized strings in EDMarketConnector's main window you should refresh them in your `prefs_changed` function in case the user has changed their preferred language. @@ -1262,11 +1271,11 @@ Any modules the core application code uses will naturally be packaged, and we explicitly include a small number of additional modules for the use of plugins. -Whilst we would like to make all of the `stdlib` of Python available it is -not automatically packaged into our releases by py2exe. We hope to address -this in the 5.3 release series. In the meantime, if there's anything -missing that you'd like to use, please ask. Yes, this very much means you -need to test your plugins against a Windows installation of the application +Whilst we would like to make all of the `stdlib` of Python available it is +not automatically packaged into our releases by py2exe. We hope to address +this in the 5.3 release series. In the meantime, if there's anything +missing that you'd like to use, please ask. Yes, this very much means you +need to test your plugins against a Windows installation of the application to be sure it will work. See @@ -1416,7 +1425,7 @@ versions of EDMarketConnector: [2to3](https://docs.python.org/3/library/2to3.html) tool can automate much of this work. -We advise *against* making any attempt to have a plugin's code work under -both Python 2.7 and 3.x. We no longer maintain the Python 2.7-based -versions of this application, and you shouldn't support use of them with +We advise *against* making any attempt to have a plugin's code work under +both Python 2.7 and 3.x. We no longer maintain the Python 2.7-based +versions of this application, and you shouldn't support use of them with your plugin. diff --git a/l10n.py b/l10n.py index a2b5185a..66d66cad 100755 --- a/l10n.py +++ b/l10n.py @@ -17,9 +17,10 @@ import re import sys import warnings from contextlib import suppress -from os import pardir, listdir, sep, makedirs -from os.path import basename, dirname, isdir, isfile, join, abspath, exists +from os import listdir, makedirs, pardir, sep +from os.path import abspath, basename, dirname, exists, isdir, isfile, join from typing import TYPE_CHECKING, Iterable, TextIO, cast + from config import config from EDMCLogging import get_main_logger @@ -154,21 +155,41 @@ class _Translations: return translations - def translate(self, x: str, context: str | None = None) -> str: + def translate(self, x: str, context: str | None = None, lang: str | None = None) -> str: # noqa: CCR001 """ - Translate the given string to the current lang. + Translate the given string to the current lang or an overriden lang. :param x: The string to translate - :param context: Whether or not to search the given directory for translation files, defaults to None + :param context: Contains the full path to the file being localised, from which the plugin name is parsed and + used to locate the plugin translation files, defaults to None + :param lang: Contains a language code to override the EDMC language for this translation, defaults to None :return: The translated string """ + plugin_name: str | None = None + plugin_path: str | None = None + if context: # TODO: There is probably a better way to go about this now. - context = context[len(config.plugin_dir)+1:].split(sep)[0] - if self.translations[None] and context not in self.translations: - logger.debug(f'No translations for {context!r}') + plugin_name = context[len(config.plugin_dir)+1:].split(sep)[0] + plugin_path = join(config.plugin_dir_path, plugin_name, LOCALISATION_DIR) - return self.translations.get(context, {}).get(x) or self.translate(x) + if lang: + contents: dict[str, str] = self.contents(lang=lang, plugin_path=plugin_path) + + if not contents or type(contents) is not dict: + logger.debug(f'Failure loading translations for overridden language {lang!r}') + return self.translate(x) + elif x not in contents.keys(): + logger.debug(f'Missing translation: {x!r} for overridden language {lang!r}') + return self.translate(x) + else: + return contents.get(x) or self.translate(x) + + if plugin_name: + if self.translations[None] and plugin_name not in self.translations: + logger.debug(f'No translations for {plugin_name!r}') + + return self.translations.get(plugin_name, {}).get(x) or self.translate(x) if self.translations[None] and x not in self.translations[None]: logger.debug(f'Missing translation: {x!r}') From 158456aba73fc9ee8a548ed2c35c6b9f4fd60b0d Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Fri, 12 Apr 2024 11:08:58 -0400 Subject: [PATCH 02/33] [2198] Remove Potentially Unused Dependencies Let's see what happens! --- FDevIDs | 2 +- config/__init__.py | 2 +- requirements.txt | 7 ------- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/FDevIDs b/FDevIDs index 9b3f4061..7cffab3d 160000 --- a/FDevIDs +++ b/FDevIDs @@ -1 +1 @@ -Subproject commit 9b3f40612017b43a8b826017e1e2befebd9074f2 +Subproject commit 7cffab3d913b788f981923687203399c22cf358f diff --git a/config/__init__.py b/config/__init__.py index 27d9b450..c9a477c4 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -53,7 +53,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.10.4' +_static_appversion = '5.11.0-alpha1' _cached_version: semantic_version.Version | None = None copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD' diff --git a/requirements.txt b/requirements.txt index 75ea4041..8c450362 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,5 @@ -certifi==2024.2.2 requests==2.31.0 pillow==10.3.0 -# requests depends on this now ? -charset-normalizer==3.3.2 - watchdog==3.0.0 -# Commented out because this doesn't package well with py2exe infi.systray==0.1.12; sys_platform == 'win32' -# argh==0.26.2 watchdog dep -# pyyaml==5.3.1 watchdog dep semantic-version==2.10.0 From 6b626fc0dabd758892a7975b0adb9ee6ad93790b Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Sat, 20 Apr 2024 17:45:14 -0400 Subject: [PATCH 03/33] Update FDevIDs --- FDevIDs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FDevIDs b/FDevIDs index 7cffab3d..9b3f4061 160000 --- a/FDevIDs +++ b/FDevIDs @@ -1 +1 @@ -Subproject commit 7cffab3d913b788f981923687203399c22cf358f +Subproject commit 9b3f40612017b43a8b826017e1e2befebd9074f2 From 080d9f98f2f1edb64ae5d2c34025667222cb495f Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 17:33:28 -0400 Subject: [PATCH 04/33] [1812] Handover Translations --- EDMC.py | 7 +- EDMarketConnector.py | 177 ++++++++++++++++---------------- companion.py | 29 +++--- journal_lock.py | 19 ++-- l10n.py | 47 +++++++-- monitor.py | 4 - plugins/coriolis.py | 65 ++++++------ plugins/eddn.py | 38 +++---- plugins/edsm.py | 33 +++--- plugins/inara.py | 25 ++--- prefs.py | 138 ++++++++++++------------- stats.py | 235 +++++++++++++++++++++---------------------- theme.py | 9 +- ttkHyperlinkLabel.py | 8 +- update.py | 4 +- 15 files changed, 415 insertions(+), 423 deletions(-) diff --git a/EDMC.py b/EDMC.py index 39d98982..64488778 100755 --- a/EDMC.py +++ b/EDMC.py @@ -26,7 +26,6 @@ from EDMCLogging import edmclogger, logger, logging if TYPE_CHECKING: from logging import TRACE # type: ignore # noqa: F401 # needed to make mypy happy - def _(x: str): return x edmclogger.set_channels_loglevel(logging.INFO) @@ -35,7 +34,7 @@ import collate import commodity import companion import edshipyard -import l10n +from l10n import translations as tr import loadout import outfitting import shipyard @@ -66,7 +65,7 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' ) -l10n.Translations.install_dummy() +tr.install_dummy() SERVER_RETRY = 5 # retry pause for Companion servers [s] EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS, \ @@ -164,7 +163,7 @@ def main(): # noqa: C901, CCR001 newversion: EDMCVersion | None = updater.check_appcast() if newversion: # LANG: Update Available Text - newverstr: str = _("{NEWVER} is available").format(NEWVER=newversion.title) + newverstr: str = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title) print(f'{appversion()} ({newverstr})') else: print(appversion()) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 3ab97eb5..09e6fc4e 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -413,9 +413,6 @@ if TYPE_CHECKING: from infi.systray import SysTrayIcon # isort: on - def _(x: str) -> str: - """Fake the l10n translation functions for typing.""" - return x import tkinter as tk import tkinter.filedialog @@ -432,7 +429,7 @@ from commodity import COMMODITY_CSV from dashboard import dashboard from edmc_data import ship_name_map from hotkey import hotkeymgr -from l10n import Translations +from l10n import translations as tr from monitor import monitor from theme import theme from ttkHyperlinkLabel import HyperlinkLabel @@ -590,7 +587,7 @@ class AppWindow: self.button = ttk.Button( frame, name='update_button', - text=_('Update'), # LANG: Main UI Update button + text=tr.tl('Update'), # LANG: Main UI Update button width=28, default=tk.ACTIVE, state=tk.DISABLED @@ -660,7 +657,7 @@ class AppWindow: self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) self.system_menu.add_separator() # LANG: Appearance - Label for checkbox to select if application always on top - self.system_menu.add_checkbutton(label=_('Always on top'), + self.system_menu.add_checkbutton(label=tr.tl('Always on top'), variable=self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) @@ -765,7 +762,7 @@ class AppWindow: # Check for Valid Providers validate_providers() if monitor.cmdr is None: - self.status['text'] = _("Awaiting Full CMDR Login") # LANG: Await Full CMDR Login to Game + self.status['text'] = tr.tl("Awaiting Full CMDR Login") # LANG: Await Full CMDR Login to Game # Start a protocol handler to handle cAPI registration. Requires main loop to be running. self.w.after_idle(lambda: protocol.protocolhandler.start(self.w)) @@ -795,7 +792,7 @@ class AppWindow: suit = monitor.state.get('SuitCurrent') if suit is None: - self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit + self.suit['text'] = f'<{tr.tl("Unknown")}>' # LANG: Unknown suit return suitname = suit['edmcName'] @@ -851,45 +848,45 @@ class AppWindow: # (Re-)install log monitoring if not monitor.start(self.w): # LANG: ED Journal file location appears to be in error - self.status['text'] = _('Error: Check E:D journal file location') + self.status['text'] = tr.tl('Error: Check E:D journal file location') if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr def set_labels(self): """Set main window labels, e.g. after language change.""" - self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Label for commander name in main window + self.cmdr_label['text'] = tr.tl('Cmdr') + ':' # LANG: Label for commander name in main window # LANG: 'Ship' or multi-crew role label in main window, as applicable - self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window - self.suit_label['text'] = _('Suit') + ':' # LANG: Label for 'Suit' line in main UI - self.system_label['text'] = _('System') + ':' # LANG: Label for 'System' line in main UI - self.station_label['text'] = _('Station') + ':' # LANG: Label for 'Station' line in main UI - self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window - self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title - self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title - self.menubar.entryconfigure(3, label=_('Help')) # LANG: 'Help' menu title - self.theme_file_menu['text'] = _('File') # LANG: 'File' menu title - self.theme_edit_menu['text'] = _('Edit') # LANG: 'Edit' menu title - self.theme_help_menu['text'] = _('Help') # LANG: 'Help' menu title + self.ship_label['text'] = (monitor.state['Captain'] and tr.tl('Role') or tr.tl('Ship')) + ':' # Main window + self.suit_label['text'] = tr.tl('Suit') + ':' # LANG: Label for 'Suit' line in main UI + self.system_label['text'] = tr.tl('System') + ':' # LANG: Label for 'System' line in main UI + self.station_label['text'] = tr.tl('Station') + ':' # LANG: Label for 'Station' line in main UI + self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window + self.menubar.entryconfigure(1, label=tr.tl('File')) # LANG: 'File' menu title + self.menubar.entryconfigure(2, label=tr.tl('Edit')) # LANG: 'Edit' menu title + self.menubar.entryconfigure(3, label=tr.tl('Help')) # LANG: 'Help' menu title + self.theme_file_menu['text'] = tr.tl('File') # LANG: 'File' menu title + self.theme_edit_menu['text'] = tr.tl('Edit') # LANG: 'Edit' menu title + self.theme_help_menu['text'] = tr.tl('Help') # LANG: 'Help' menu title # File menu - self.file_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status - self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: File > Save Raw Data... - self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: File > Settings - self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: File > Exit + self.file_menu.entryconfigure(0, label=tr.tl('Status')) # LANG: File > Status + self.file_menu.entryconfigure(1, label=tr.tl('Save Raw Data...')) # LANG: File > Save Raw Data... + self.file_menu.entryconfigure(2, label=tr.tl('Settings')) # LANG: File > Settings + self.file_menu.entryconfigure(4, label=tr.tl('Exit')) # LANG: File > Exit # Help menu - self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help > Documentation - self.help_menu.entryconfigure(1, label=_('Troubleshooting')) # LANG: Help > Troubleshooting - self.help_menu.entryconfigure(2, label=_('Report A Bug')) # LANG: Help > Report A Bug - self.help_menu.entryconfigure(3, label=_('Privacy Policy')) # LANG: Help > Privacy Policy - self.help_menu.entryconfigure(4, label=_('Release Notes')) # LANG: Help > Release Notes - self.help_menu.entryconfigure(5, label=_('Check for Updates...')) # LANG: Help > Check for Updates... - self.help_menu.entryconfigure(6, label=_("About {APP}").format(APP=applongname)) # LANG: Help > About App - self.help_menu.entryconfigure(7, label=_('Open Log Folder')) # LANG: Help > Open Log Folder + self.help_menu.entryconfigure(0, label=tr.tl('Documentation')) # LANG: Help > Documentation + self.help_menu.entryconfigure(1, label=tr.tl('Troubleshooting')) # LANG: Help > Troubleshooting + self.help_menu.entryconfigure(2, label=tr.tl('Report A Bug')) # LANG: Help > Report A Bug + self.help_menu.entryconfigure(3, label=tr.tl('Privacy Policy')) # LANG: Help > Privacy Policy + self.help_menu.entryconfigure(4, label=tr.tl('Release Notes')) # LANG: Help > Release Notes + self.help_menu.entryconfigure(5, label=tr.tl('Check for Updates...')) # LANG: Help > Check for Updates... + self.help_menu.entryconfigure(6, label=tr.tl("About {APP}").format(APP=applongname)) # LANG: Help > About App + self.help_menu.entryconfigure(7, label=tr.tl('Open Log Folder')) # LANG: Help > Open Log Folder # Edit menu - self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: Label for 'Copy' as in 'Copy and Paste' + self.edit_menu.entryconfigure(0, label=tr.tl('Copy')) # LANG: Label for 'Copy' as in 'Copy and Paste' def login(self): """Initiate CAPI/Frontier login and set other necessary state.""" @@ -900,12 +897,12 @@ class AppWindow: if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') # LANG: CAPI auth aborted because of killswitch - self.status['text'] = _('CAPI auth disabled by killswitch') + self.status['text'] = tr.tl('CAPI auth disabled by killswitch') return if not self.status['text']: # LANG: Status - Attempting to get a Frontier Auth Access Token - self.status['text'] = _('Logging in...') + self.status['text'] = tr.tl('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED @@ -916,7 +913,7 @@ class AppWindow: try: if companion.session.login(monitor.cmdr, monitor.is_beta): # LANG: Successfully authenticated with the Frontier website - self.status['text'] = _('Authentication successful') + self.status['text'] = tr.tl('Authentication successful') self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data @@ -947,17 +944,17 @@ class AppWindow: # Signal as error because the user might actually be docked # but the server hosting the Companion API hasn't caught up # LANG: Player is not docked at a station, when we expect them to be - self._handle_status(_("You're not docked at a station!")) + self._handle_status(tr.tl("You're not docked at a station!")) return False # Ignore possibly missing shipyard info if output_flags & config.OUT_EDDN_SEND_STATION_DATA and not (has_commodities or has_modules): # LANG: Status - Either no market or no modules data for station from Frontier CAPI - self._handle_status(_("Station doesn't have anything!")) + self._handle_status(tr.tl("Station doesn't have anything!")) elif not has_commodities: # LANG: Status - No station market data from Frontier CAPI - self._handle_status(_("Station doesn't have a market!")) + self._handle_status(tr.tl("Station doesn't have a market!")) elif output_flags & commodities_flag: # Fixup anomalies in the comodity data @@ -995,7 +992,7 @@ class AppWindow: if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') # LANG: CAPI auth query aborted because of killswitch - self.status['text'] = _('CAPI auth disabled by killswitch') + self.status['text'] = tr.tl('CAPI auth disabled by killswitch') hotkeymgr.play_bad() return @@ -1005,37 +1002,37 @@ class AppWindow: if not monitor.cmdr: logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown') # LANG: CAPI queries aborted because Cmdr name is unknown - self.status['text'] = _('CAPI query aborted: Cmdr name unknown') + self.status['text'] = tr.tl('CAPI query aborted: Cmdr name unknown') return if not monitor.mode: logger.trace_if('capi.worker', 'Aborting Query: Game Mode unknown') # LANG: CAPI queries aborted because game mode unknown - self.status['text'] = _('CAPI query aborted: Game mode unknown') + self.status['text'] = tr.tl('CAPI query aborted: Game mode unknown') return if monitor.state['GameVersion'] is None: logger.trace_if('capi.worker', 'Aborting Query: GameVersion unknown') # LANG: CAPI queries aborted because GameVersion unknown - self.status['text'] = _('CAPI query aborted: GameVersion unknown') + self.status['text'] = tr.tl('CAPI query aborted: GameVersion unknown') return if not monitor.state['SystemName']: logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown') # LANG: CAPI queries aborted because current star system name unknown - self.status['text'] = _('CAPI query aborted: Current system unknown') + self.status['text'] = tr.tl('CAPI query aborted: Current system unknown') return if monitor.state['Captain']: logger.trace_if('capi.worker', 'Aborting Query: In multi-crew') # LANG: CAPI queries aborted because player is in multi-crew on other Cmdr's ship - self.status['text'] = _('CAPI query aborted: In other-ship multi-crew') + self.status['text'] = tr.tl('CAPI query aborted: In other-ship multi-crew') return if monitor.mode == 'CQC': logger.trace_if('capi.worker', 'Aborting Query: In CQC') # LANG: CAPI queries aborted because player is in CQC (Arena) - self.status['text'] = _('CAPI query aborted: CQC (Arena) detected') + self.status['text'] = tr.tl('CAPI query aborted: CQC (Arena) detected') return if companion.session.state == companion.Session.STATE_AUTH: @@ -1056,7 +1053,7 @@ class AppWindow: hotkeymgr.play_good() # LANG: Status - Attempting to retrieve data from Frontier CAPI - self.status['text'] = _('Fetching data...') + self.status['text'] = tr.tl('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() @@ -1086,20 +1083,20 @@ class AppWindow: if should_return: logger.warning('capi.fleetcarrier has been disabled via killswitch. Returning.') # LANG: CAPI fleetcarrier query aborted because of killswitch - self.status['text'] = _('CAPI fleetcarrier disabled by killswitch') + self.status['text'] = tr.tl('CAPI fleetcarrier disabled by killswitch') hotkeymgr.play_bad() return if not monitor.cmdr: logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown') # LANG: CAPI fleetcarrier query aborted because Cmdr name is unknown - self.status['text'] = _('CAPI query aborted: Cmdr name unknown') + self.status['text'] = tr.tl('CAPI query aborted: Cmdr name unknown') return if monitor.state['GameVersion'] is None: logger.trace_if('capi.worker', 'Aborting Query: GameVersion unknown') # LANG: CAPI fleetcarrier query aborted because GameVersion unknown - self.status['text'] = _('CAPI query aborted: GameVersion unknown') + self.status['text'] = tr.tl('CAPI query aborted: GameVersion unknown') return if not companion.session.retrying: @@ -1108,7 +1105,7 @@ class AppWindow: return # LANG: Status - Attempting to retrieve data from Frontier CAPI - self.status['text'] = _('Fetching data...') + self.status['text'] = tr.tl('Fetching data...') self.w.update_idletasks() query_time = int(time()) @@ -1151,11 +1148,11 @@ class AppWindow: # Validation if 'name' not in capi_response.capi_data: # LANG: No data was returned for the fleetcarrier from the Frontier CAPI - err = self.status['text'] = _('CAPI: No fleetcarrier data returned') + err = self.status['text'] = tr.tl('CAPI: No fleetcarrier data returned') elif not capi_response.capi_data.get('name', {}).get('callsign'): # LANG: We didn't have the fleetcarrier callsign when we should have - err = self.status['text'] = _("CAPI: Fleetcarrier data incomplete") # Shouldn't happen + err = self.status['text'] = tr.tl("CAPI: Fleetcarrier data incomplete") # Shouldn't happen else: if __debug__: # Recording @@ -1174,24 +1171,24 @@ class AppWindow: elif 'commander' not in capi_response.capi_data: # This can happen with EGS Auth if no commander created yet # LANG: No data was returned for the commander from the Frontier CAPI - err = self.status['text'] = _('CAPI: No commander data returned') + err = self.status['text'] = tr.tl('CAPI: No commander data returned') elif not capi_response.capi_data.get('commander', {}).get('name'): # LANG: We didn't have the commander name when we should have - err = self.status['text'] = _("Who are you?!") # Shouldn't happen + err = self.status['text'] = tr.tl("Who are you?!") # Shouldn't happen elif (not capi_response.capi_data.get('lastSystem', {}).get('name') or (capi_response.capi_data['commander'].get('docked') and not capi_response.capi_data.get('lastStarport', {}).get('name'))): # LANG: We don't know where the commander is, when we should - err = self.status['text'] = _("Where are you?!") # Shouldn't happen + err = self.status['text'] = tr.tl("Where are you?!") # Shouldn't happen elif ( not capi_response.capi_data.get('ship', {}).get('name') or not capi_response.capi_data.get('ship', {}).get('modules') ): # LANG: We don't know what ship the commander is in, when we should - err = self.status['text'] = _("What are you flying?!") # Shouldn't happen + err = self.status['text'] = tr.tl("What are you flying?!") # Shouldn't happen elif monitor.cmdr and capi_response.capi_data['commander']['name'] != monitor.cmdr: # Companion API Commander doesn't match Journal @@ -1318,7 +1315,7 @@ class AppWindow: except companion.ServerConnectionError as comp_err: # LANG: Frontier CAPI server error when fetching data - self.status['text'] = _('Frontier CAPI server error') + self.status['text'] = tr.tl('Frontier CAPI server error') logger.warning(f'Exception while contacting server: {comp_err}') err = self.status['text'] = str(comp_err) play_bad = True @@ -1327,7 +1324,7 @@ class AppWindow: # We need to 'close' the auth else it'll see STATE_OK and think login() isn't needed companion.session.reinit_session() # LANG: Frontier CAPI Access Token expired, trying to get a new one - self.status['text'] = _('CAPI: Refreshing access token...') + self.status['text'] = tr.tl('CAPI: Refreshing access token...') if companion.session.login(): logger.debug('Initial query failed, but login() just worked, trying again...') companion.session.retrying = True @@ -1366,7 +1363,7 @@ class AppWindow: if not err: # not self.status['text']: # no errors # LANG: Time when we last obtained Frontier CAPI data - self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(capi_response.query_time)) + self.status['text'] = strftime(tr.tl('Last updated at %H:%M:%S'), localtime(capi_response.query_time)) if capi_response.play_sound and play_bad: hotkeymgr.play_bad() @@ -1394,9 +1391,9 @@ class AppWindow: return { None: '', 'Idle': '', - 'FighterCon': _('Fighter'), # LANG: Multicrew role - 'FireCon': _('Gunner'), # LANG: Multicrew role - 'FlightCon': _('Helm'), # LANG: Multicrew role + 'FighterCon': tr.tl('Fighter'), # LANG: Multicrew role + 'FireCon': tr.tl('Gunner'), # LANG: Multicrew role + 'FlightCon': tr.tl('Helm'), # LANG: Multicrew role }.get(role, role) if monitor.thread is None: @@ -1419,7 +1416,7 @@ class AppWindow: else: self.cmdr['text'] = f'{monitor.cmdr}' - self.ship_label['text'] = _('Role') + ':' # LANG: Multicrew role label in main window + self.ship_label['text'] = tr.tl('Role') + ':' # LANG: Multicrew role label in main window self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None) elif monitor.cmdr: @@ -1429,7 +1426,7 @@ class AppWindow: else: self.cmdr['text'] = monitor.cmdr - self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI + self.ship_label['text'] = tr.tl('Ship') + ':' # LANG: 'Ship' label in main UI # TODO: Show something else when on_foot if monitor.state['ShipName']: @@ -1452,7 +1449,7 @@ class AppWindow: else: self.cmdr['text'] = '' - self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI + self.ship_label['text'] = tr.tl('Ship') + ':' # LANG: 'Ship' label in main UI self.ship['text'] = '' if monitor.cmdr and monitor.is_beta: @@ -1589,7 +1586,7 @@ class AppWindow: try: companion.session.auth_callback() # LANG: Successfully authenticated with the Frontier website - self.status['text'] = _('Authentication successful') + self.status['text'] = tr.tl('Authentication successful') self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data @@ -1675,10 +1672,10 @@ class AppWindow: # Update button in main window cooldown_time = int(self.capi_query_holdoff_time - time()) # LANG: Cooldown on 'Update' button - self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS=cooldown_time) + self.button['text'] = self.theme_button['text'] = tr.tl('cooldown {SS}s').format(SS=cooldown_time) self.w.after(1000, self.cooldown) else: - self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window + self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window self.button['state'] = self.theme_button['state'] = ( monitor.cmdr and monitor.mode and @@ -1743,7 +1740,7 @@ class AppWindow: self.parent = parent # LANG: Help > About App - self.title(_('About {APP}').format(APP=applongname)) + self.title(tr.tl('About {APP}').format(APP=applongname)) if parent.winfo_viewable(): self.transient(parent) @@ -1780,7 +1777,7 @@ class AppWindow: self.appversion_label.config(state=tk.DISABLED, bg=frame.cget("background"), font="TkDefaultFont") self.appversion_label.grid(row=row, column=0, sticky=tk.E) # LANG: Help > Release Notes - self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'), + self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=tr.tl('Release Notes'), url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/' f'{appversion_nobuild()}', underline=True) @@ -1806,7 +1803,7 @@ class AppWindow: ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 # LANG: Generic 'OK' button label - button = ttk.Button(frame, text=_('OK'), command=self.apply) + button = ttk.Button(frame, text=tr.tl('OK'), command=self.apply) button.grid(row=row, column=2, sticky=tk.E) button.bind("", lambda event: self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) @@ -1874,7 +1871,7 @@ class AppWindow: # Let the user know we're shutting down. # LANG: The application is shutting down - self.status['text'] = _('Shutting down...') + self.status['text'] = tr.tl('Shutting down...') self.w.update_idletasks() logger.info('Starting shutdown procedures...') @@ -2059,10 +2056,10 @@ def validate_providers(): return # LANG: Popup-text about Reset Providers - popup_text = _(r'One or more of your URL Providers were invalid, and have been reset:\r\n\r\n') + popup_text = tr.tl(r'One or more of your URL Providers were invalid, and have been reset:\r\n\r\n') for provider in reset_providers: # LANG: Text About What Provider Was Reset - popup_text += _(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n') + popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n') popup_text = popup_text.format( PROVIDER=provider, OLDPROV=reset_providers[provider][0], @@ -2074,7 +2071,7 @@ def validate_providers(): tk.messagebox.showinfo( # LANG: Popup window title for Reset Providers - _('EDMC: Default Providers Reset'), + tr.tl('EDMC: Default Providers Reset'), popup_text ) @@ -2200,7 +2197,7 @@ sys.path: {sys.path}''' # Plain, not via `logger` print(f'{applongname} {appversion()}') - Translations.install(config.get_str('language')) # Can generate errors so wait til log set up + tr.install(config.get_str('language')) # Can generate errors so wait til log set up setup_killswitches(args.killswitches_file) @@ -2235,7 +2232,7 @@ sys.path: {sys.path}''' """Display message about 'broken' plugins that failed to load.""" if plug.PLUGINS_broken: # LANG: Popup-text about 'broken' plugins that failed to load - popup_text = _( + popup_text = tr.tl( "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' " "tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py " r"file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by " @@ -2244,9 +2241,9 @@ sys.path: {sys.path}''' # Substitute in the other words. popup_text = popup_text.format( - PLUGINS=_('Plugins'), # LANG: Settings > Plugins tab - FILE=_('File'), # LANG: 'File' menu - SETTINGS=_('Settings'), # LANG: File > Settings + PLUGINS=tr.tl('Plugins'), # LANG: Settings > Plugins tab + FILE=tr.tl('File'), # LANG: 'File' menu + SETTINGS=tr.tl('Settings'), # LANG: File > Settings DISABLED='.disabled' ) # And now we do need these to be actual \r\n @@ -2255,7 +2252,7 @@ sys.path: {sys.path}''' tk.messagebox.showinfo( # LANG: Popup window title for list of 'broken' plugins that failed to load - _('EDMC: Broken Plugins'), + tr.tl('EDMC: Broken Plugins'), popup_text ) @@ -2264,7 +2261,7 @@ sys.path: {sys.path}''' plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0) if (plugins_not_py3_last + 86400) < int(time()) and plug.PLUGINS_not_py3: # LANG: Popup-text about 'active' plugins without Python 3.x support - popup_text = _( + popup_text = tr.tl( "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the " "list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an " "updated version available, else alert the developer that they need to update the code for " @@ -2274,9 +2271,9 @@ sys.path: {sys.path}''' # Substitute in the other words. popup_text = popup_text.format( - PLUGINS=_('Plugins'), # LANG: Settings > Plugins tab - FILE=_('File'), # LANG: 'File' menu - SETTINGS=_('Settings'), # LANG: File > Settings + PLUGINS=tr.tl('Plugins'), # LANG: Settings > Plugins tab + FILE=tr.tl('File'), # LANG: 'File' menu + SETTINGS=tr.tl('Settings'), # LANG: File > Settings DISABLED='.disabled' ) # And now we do need these to be actual \r\n @@ -2285,7 +2282,7 @@ sys.path: {sys.path}''' tk.messagebox.showinfo( # LANG: Popup window title for list of 'enabled' plugins that don't work with Python 3.x - _('EDMC: Plugins Without Python 3.x Support'), + tr.tl('EDMC: Plugins Without Python 3.x Support'), popup_text ) config.set('plugins_not_py3_last', int(time())) @@ -2298,7 +2295,7 @@ sys.path: {sys.path}''' if fdevid_file.is_file(): continue # LANG: Popup-text about missing FDEVID Files - popup_text = _( + popup_text = tr.tl( "FDevID Files not found! Some functionality regarding commodities " r"may be disabled.\r\n\r\n Do you want to open the Wiki page on " "how to set up submodules?" @@ -2309,7 +2306,7 @@ sys.path: {sys.path}''' openwikipage = tk.messagebox.askquestion( # LANG: Popup window title for missing FDEVID files - _('FDevIDs: Missing Commodity Files'), + tr.tl('FDevIDs: Missing Commodity Files'), popup_text ) if openwikipage == "yes": diff --git a/companion.py b/companion.py index d0030748..1b9c693a 100644 --- a/companion.py +++ b/companion.py @@ -37,12 +37,11 @@ from config import config, user_agent from edmc_data import companion_category_map as category_map from EDMCLogging import get_main_logger from monitor import monitor +from l10n import translations as tr logger = get_main_logger() if TYPE_CHECKING: - def _(x: str): return x - UserDict = collections.UserDict[str, Any] # indicate to our type checkers what this generic class holds normally else: UserDict = collections.UserDict # Otherwise simply use the actual class @@ -224,7 +223,7 @@ class ServerError(Exception): self.args = args if not args: # LANG: Frontier CAPI didn't respond - self.args = (_("Error: Frontier CAPI didn't respond"),) + self.args = (tr.tl("Error: Frontier CAPI didn't respond"),) class ServerConnectionError(ServerError): @@ -243,7 +242,7 @@ class ServerLagging(Exception): self.args = args if not args: # LANG: Frontier CAPI data doesn't agree with latest Journal game location - self.args = (_('Error: Frontier server is lagging'),) + self.args = (tr.tl('Error: Frontier server is lagging'),) class NoMonitorStation(Exception): @@ -259,7 +258,7 @@ class NoMonitorStation(Exception): self.args = args if not args: # LANG: Commander is docked at an EDO settlement, got out and back in, we forgot the station - self.args = (_("Docked but unknown station: EDO Settlement?"),) + self.args = (tr.tl("Docked but unknown station: EDO Settlement?"),) class CredentialsError(Exception): @@ -269,7 +268,7 @@ class CredentialsError(Exception): self.args = args if not args: # LANG: Generic "something went wrong with Frontier Auth" error - self.args = (_('Error: Invalid Credentials'),) + self.args = (tr.tl('Error: Invalid Credentials'),) class CredentialsRequireRefresh(Exception): @@ -294,7 +293,7 @@ class CmdrError(Exception): self.args = args if not args: # LANG: Frontier CAPI authorisation not for currently game-active commander - self.args = (_('Error: Wrong Cmdr'),) + self.args = (tr.tl('Error: Wrong Cmdr'),) class Auth: @@ -429,7 +428,7 @@ class Auth: '' ) # LANG: Generic error prefix - following text is from Frontier auth service - raise CredentialsError(f'{_("Error")}: {error!r}') + raise CredentialsError(f'{tr.tl("Error")}: {error!r}') r = None try: @@ -472,18 +471,18 @@ class Auth: if (usr := data_decode.get('usr')) is None: logger.error('No "usr" in /decode data') # LANG: Frontier auth, no 'usr' section in returned data - raise CredentialsError(_("Error: Couldn't check token customer_id")) + raise CredentialsError(tr.tl("Error: Couldn't check token customer_id")) if (customer_id := usr.get('customer_id')) is None: logger.error('No "usr"->"customer_id" in /decode data') # LANG: Frontier auth, no 'customer_id' in 'usr' section in returned data - raise CredentialsError(_("Error: Couldn't check token customer_id")) + raise CredentialsError(tr.tl("Error: Couldn't check token customer_id")) # All 'FID' seen in Journals so far have been 'F' # Frontier, Steam and Epic if f'F{customer_id}' != monitor.state.get('FID'): # LANG: Frontier auth customer_id doesn't match game session FID - raise CredentialsError(_("Error: customer_id doesn't match!")) + raise CredentialsError(tr.tl("Error: customer_id doesn't match!")) logger.info(f'Frontier CAPI Auth: New token for \"{self.cmdr}\"') cmdrs = config.get_list('cmdrs', default=[]) @@ -505,7 +504,7 @@ class Auth: self.dump(r) # LANG: Failed to get Access Token from Frontier Auth service - raise CredentialsError(_('Error: unable to get token')) from e + raise CredentialsError(tr.tl('Error: unable to get token')) from e logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") self.dump(r) @@ -514,7 +513,7 @@ class Auth: '' ) # LANG: Generic error prefix - following text is from Frontier auth service - raise CredentialsError(f'{_("Error")}: {error!r}') + raise CredentialsError(f'{tr.tl("Error")}: {error!r}') @staticmethod def invalidate(cmdr: str | None) -> None: @@ -841,7 +840,7 @@ class Session: except Exception as e: logger.debug('Attempting GET', exc_info=e) # LANG: Frontier CAPI data retrieval failed - raise ServerError(f'{_("Frontier CAPI query failure")}: {capi_endpoint}') from e + raise ServerError(f'{tr.tl("Frontier CAPI query failure")}: {capi_endpoint}') from e if capi_endpoint == self.FRONTIER_CAPI_PATH_PROFILE and 'commander' not in capi_data: logger.error('No commander in returned data') @@ -874,7 +873,7 @@ class Session: if response.status_code == 418: # "I'm a teapot" - used to signal maintenance # LANG: Frontier CAPI returned 418, meaning down for maintenance - raise ServerError(_("Frontier CAPI down for maintenance")) + raise ServerError(tr.tl("Frontier CAPI down for maintenance")) logger.exception('Frontier CAPI: Misc. Error') raise ServerError('Frontier CAPI: Misc. Error') diff --git a/journal_lock.py b/journal_lock.py index 3a4dad52..2aae20a6 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -13,17 +13,13 @@ import tkinter as tk from enum import Enum from os import getpid as os_getpid from tkinter import ttk -from typing import TYPE_CHECKING, Callable - +from typing import Callable +from l10n import translations as tr from config import config from EDMCLogging import get_main_logger logger = get_main_logger() -if TYPE_CHECKING: # pragma: no cover - def _(x: str) -> str: - return x - class JournalLockResult(Enum): """Enumeration of possible outcomes of trying to lock the Journal Directory.""" @@ -212,7 +208,7 @@ class JournalLock: self.parent = parent self.callback = callback # LANG: Title text on popup when Journal directory already locked - self.title(_('Journal directory already locked')) + self.title(tr.tl('Journal directory already locked')) # remove decoration if sys.platform == 'win32': @@ -225,16 +221,17 @@ class JournalLock: self.blurb = tk.Label(frame) # LANG: Text for when newly selected Journal directory is already locked - self.blurb['text'] = _("The new Journal Directory location is already locked.{CR}" - "You can either attempt to resolve this and then Retry, or choose to Ignore this.") + self.blurb['text'] = tr.tl("The new Journal Directory location is already locked.{CR}" + "You can either attempt to resolve this and then Retry, " + "or choose to Ignore this.") self.blurb.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW) # LANG: Generic 'Retry' button label - self.retry_button = ttk.Button(frame, text=_('Retry'), command=self.retry) + self.retry_button = ttk.Button(frame, text=tr.tl('Retry'), command=self.retry) self.retry_button.grid(row=2, column=0, sticky=tk.EW) # LANG: Generic 'Ignore' button label - self.ignore_button = ttk.Button(frame, text=_('Ignore'), command=self.ignore) + self.ignore_button = ttk.Button(frame, text=tr.tl('Ignore'), command=self.ignore) self.ignore_button.grid(row=2, column=1, sticky=tk.EW) self.protocol("WM_DELETE_WINDOW", self._destroy) diff --git a/l10n.py b/l10n.py index 183d902a..f3215062 100755 --- a/l10n.py +++ b/l10n.py @@ -23,9 +23,6 @@ from typing import TYPE_CHECKING, Iterable, TextIO, cast from config import config from EDMCLogging import get_main_logger -if TYPE_CHECKING: - def _(x: str) -> str: return x - # Note that this is also done in EDMarketConnector.py, and thus removing this here may not have a desired effect try: locale.setlocale(locale.LC_ALL, '') @@ -61,7 +58,16 @@ if sys.platform == 'win32': GetNumberFormatEx.restype = ctypes.c_int -class _Translations: +class Translations: + """ + The Translation System. + + Contains all the logic needed to support multiple languages in EDMC. + DO NOT USE THIS DIRECTLY UNLESS YOU KNOW WHAT YOU'RE DOING. + In most cases, you'll want to import translations. + + For most cases: from l10n import translations as tr. + """ FALLBACK = 'en' # strings in this code are in English FALLBACK_NAME = 'English' @@ -79,6 +85,8 @@ class _Translations: Use when translation is not desired or not available """ self.translations = {None: {}} + # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. + # Migrate to calling Translations.translate directly. builtins.__dict__['_'] = lambda x: str(x).replace(r'\"', '"').replace('{CR}', '\n') def install(self, lang: str | None = None) -> None: # noqa: CCR001 @@ -88,7 +96,7 @@ class _Translations: :param lang: The language to translate to, defaults to the preferred language """ available = self.available() - available.add(_Translations.FALLBACK) + available.add(Translations.FALLBACK) if not lang: # Choose the default language for preferred in Locale.preferred_languages(): @@ -122,6 +130,8 @@ class _Translations: except Exception: logger.exception(f'Exception occurred while parsing {lang}.strings in plugin {plugin}') + # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. + # Migrate to calling Translations.translate directly. builtins.__dict__['_'] = self.translate def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]: @@ -135,12 +145,12 @@ class _Translations: for line in h: if line.strip(): - match = _Translations.TRANS_RE.match(line) + match = Translations.TRANS_RE.match(line) if match: to_set = match.group(2).replace(r'\"', '"').replace('{CR}', '\n') translations[match.group(1).replace(r'\"', '"')] = to_set - elif not _Translations.COMMENT_RE.match(line): + elif not Translations.COMMENT_RE.match(line): logger.debug(f'Bad translation: {line.strip()}') h.close() @@ -149,6 +159,10 @@ class _Translations: return translations + def tl(self, x: str, context: str | None = None) -> str: + """Use the shorthand Dummy loader for the translate function.""" + return self.translate(x, context) + def translate(self, x: str, context: str | None = None) -> str: """ Translate the given string to the current lang. @@ -182,11 +196,11 @@ class _Translations: """Available language names by code.""" names: dict[str | None, str] = { # LANG: The system default language choice in Settings > Appearance - None: _('Default'), # Appearance theme and language setting + None: self.tl('Default'), # Appearance theme and language setting } names.update(sorted( [(lang, self.contents(lang).get(LANGUAGE_ID, lang)) for lang in self.available()] + - [(_Translations.FALLBACK, _Translations.FALLBACK_NAME)], + [(Translations.FALLBACK, Translations.FALLBACK_NAME)], key=lambda x: x[1] )) # Sort by name @@ -324,9 +338,22 @@ class _Locale: # singletons Locale = _Locale() -Translations = _Translations() +translations = Translations() +# WARNING: 'Translations' singleton is deprecated. Will be removed in 6.0 or later. +# Migrate to importing 'translations'. +# Begin Deprecation Zone +class _Translations(Translations): + def __init__(self): + logger.warning(DeprecationWarning('Translations and _Translations() are deprecated. ' + 'Please use translations and Translations() instead.')) + super().__init__() + + +Translations: Translations = _Translations() # type: ignore # Yes, I know this is awful. But we need it for compat. +# End Deprecation Zone + # generate template strings file - like xgettext # parsing is limited - only single ' or " delimited strings, and only one string per line if __name__ == "__main__": diff --git a/monitor.py b/monitor.py index 02b7f91c..f93473ba 100644 --- a/monitor.py +++ b/monitor.py @@ -34,10 +34,6 @@ STARTUP = 'journal.startup' MAX_NAVROUTE_DISCREPANCY = 5 # Timestamp difference in seconds MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds -if TYPE_CHECKING: - def _(x: str) -> str: - return x - if sys.platform == 'win32': import ctypes from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR diff --git a/plugins/coriolis.py b/plugins/coriolis.py index 07b4ac9f..a9eabced 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -27,15 +27,11 @@ import io import json import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING import myNotebook as nb # noqa: N813 # its not my fault. from EDMCLogging import get_main_logger from plug import show_error from config import config - -if TYPE_CHECKING: - def _(s: str) -> str: - ... +from l10n import translations as tr class CoriolisConfig: @@ -45,15 +41,15 @@ class CoriolisConfig: self.normal_url = '' self.beta_url = '' self.override_mode = '' - self.override_text_old_auto = _('Auto') # LANG: Coriolis normal/beta selection - auto - self.override_text_old_normal = _('Normal') # LANG: Coriolis normal/beta selection - normal - self.override_text_old_beta = _('Beta') # LANG: Coriolis normal/beta selection - beta + self.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto + self.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal + self.override_text_old_beta = tr.tl('Beta') # LANG: Coriolis normal/beta selection - beta self.normal_textvar = tk.StringVar() self.beta_textvar = tk.StringVar() self.override_textvar = tk.StringVar() - def initialize_urls(self): + def initialize_urls(self) -> None: """Initialize Coriolis URLs and override mode from configuration.""" self.normal_url = config.get_str('coriolis_normal_url', default=DEFAULT_NORMAL_URL) self.beta_url = config.get_str('coriolis_beta_url', default=DEFAULT_BETA_URL) @@ -63,10 +59,10 @@ class CoriolisConfig: self.beta_textvar.set(value=self.beta_url) self.override_textvar.set( value={ - 'auto': _('Auto'), # LANG: 'Auto' label for Coriolis site override selection - 'normal': _('Normal'), # LANG: 'Normal' label for Coriolis site override selection - 'beta': _('Beta') # LANG: 'Beta' label for Coriolis site override selection - }.get(self.override_mode, _('Auto')) # LANG: 'Auto' label for Coriolis site override selection + 'auto': tr.tl('Auto'), # LANG: 'Auto' label for Coriolis site override selection + 'normal': tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + 'beta': tr.tl('Beta') # LANG: 'Beta' label for Coriolis site override selection + }.get(self.override_mode, tr.tl('Auto')) # LANG: 'Auto' label for Coriolis site override selection ) @@ -91,38 +87,38 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr BOXY = 2 # noqa: N806 # box spacing # Save the old text values for the override mode, so we can update them if the language is changed - coriolis_config.override_text_old_auto = _('Auto') # LANG: Coriolis normal/beta selection - auto - coriolis_config.override_text_old_normal = _('Normal') # LANG: Coriolis normal/beta selection - normal - coriolis_config.override_text_old_beta = _('Beta') # LANG: Coriolis normal/beta selection - beta + coriolis_config.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto + coriolis_config.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal + coriolis_config.override_text_old_beta = tr.tl('Beta') # LANG: Coriolis normal/beta selection - beta conf_frame = nb.Frame(parent) conf_frame.columnconfigure(index=1, weight=1) cur_row = 0 # LANG: Settings>Coriolis: Help/hint for changing coriolis URLs - nb.Label(conf_frame, text=_( + nb.Label(conf_frame, text=tr.tl( "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" )).grid(sticky=tk.EW, row=cur_row, column=0, padx=PADX, pady=PADY, columnspan=3) cur_row += 1 # LANG: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL - nb.Label(conf_frame, text=_('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + nb.Label(conf_frame, text=tr.tl('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) nb.EntryMenu(conf_frame, textvariable=coriolis_config.normal_textvar).grid( sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY ) # LANG: Generic 'Reset' button label - nb.Button(conf_frame, text=_("Reset"), + nb.Button(conf_frame, text=tr.tl("Reset"), command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) cur_row += 1 # LANG: Settings>Coriolis: Label for 'alpha/beta game version' URL - nb.Label(conf_frame, text=_('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + nb.Label(conf_frame, text=tr.tl('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) nb.EntryMenu(conf_frame, textvariable=coriolis_config.beta_textvar).grid( sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY ) # LANG: Generic 'Reset' button label - nb.Button(conf_frame, text=_('Reset'), + nb.Button(conf_frame, text=tr.tl('Reset'), command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) @@ -130,16 +126,16 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr # TODO: This needs a help/hint text to be sure users know what it's for. # LANG: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL - nb.Label(conf_frame, text=_('Override Beta/Normal Selection')).grid( + nb.Label(conf_frame, text=tr.tl('Override Beta/Normal Selection')).grid( sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY ) nb.OptionMenu( conf_frame, coriolis_config.override_textvar, coriolis_config.override_textvar.get(), - _('Normal'), # LANG: 'Normal' label for Coriolis site override selection - _('Beta'), # LANG: 'Beta' label for Coriolis site override selection - _('Auto') # LANG: 'Auto' label for Coriolis site override selection + tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + tr.tl('Beta'), # LANG: 'Beta' label for Coriolis site override selection + tr.tl('Auto') # LANG: 'Auto' label for Coriolis site override selection ).grid(sticky=tk.W, row=cur_row, column=1, padx=PADX, pady=BOXY) cur_row += 1 @@ -159,9 +155,9 @@ def prefs_changed(cmdr: str | None, is_beta: bool) -> None: # Convert to unlocalised names coriolis_config.override_mode = { - _('Normal'): 'normal', # LANG: Coriolis normal/beta selection - normal - _('Beta'): 'beta', # LANG: Coriolis normal/beta selection - beta - _('Auto'): 'auto', # LANG: Coriolis normal/beta selection - auto + tr.tl('Normal'): 'normal', # LANG: Coriolis normal/beta selection - normal + tr.tl('Beta'): 'beta', # LANG: Coriolis normal/beta selection - beta + tr.tl('Auto'): 'auto', # LANG: Coriolis normal/beta selection - auto }.get(coriolis_config.override_mode, coriolis_config.override_mode) # Check if the language was changed and the override_mode was valid before the change @@ -175,18 +171,19 @@ def prefs_changed(cmdr: str | None, is_beta: bool) -> None: if coriolis_config.override_mode in ('beta', 'normal', 'auto'): coriolis_config.override_textvar.set( value={ - 'auto': _('Auto'), # LANG: 'Auto' label for Coriolis site override selection - 'normal': _('Normal'), # LANG: 'Normal' label for Coriolis site override selection - 'beta': _('Beta') # LANG: 'Beta' label for Coriolis site override selection + 'auto': tr.tl('Auto'), # LANG: 'Auto' label for Coriolis site override selection + 'normal': tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + 'beta': tr.tl('Beta') # LANG: 'Beta' label for Coriolis site override selection # LANG: 'Auto' label for Coriolis site override selection - }.get(coriolis_config.override_mode, _('Auto')) + }.get(coriolis_config.override_mode, tr.tl('Auto')) ) # If the override mode is still invalid, default to auto if coriolis_config.override_mode not in ('beta', 'normal', 'auto'): logger.warning(f'Unexpected value {coriolis_config.override_mode=!r}. Defaulting to "auto"') coriolis_config.override_mode = 'auto' - coriolis_config.override_textvar.set(value=_('Auto')) # LANG: 'Auto' label for Coriolis site override selection + # LANG: 'Auto' label for Coriolis site override selection + coriolis_config.override_textvar.set(value=tr.tl('Auto')) config.set('coriolis_normal_url', coriolis_config.normal_url) config.set('coriolis_beta_url', coriolis_config.beta_url) @@ -196,7 +193,7 @@ def prefs_changed(cmdr: str | None, is_beta: bool) -> None: def _get_target_url(is_beta: bool) -> str: if coriolis_config.override_mode not in ('auto', 'normal', 'beta'): # LANG: Settings>Coriolis - invalid override mode found - show_error(_('Invalid Coriolis override mode!')) + show_error(tr.tl('Invalid Coriolis override mode!')) logger.warning(f'Unexpected override mode {coriolis_config.override_mode!r}! defaulting to auto!') coriolis_config.override_mode = 'auto' if coriolis_config.override_mode == 'beta': diff --git a/plugins/eddn.py b/plugins/eddn.py index 9504d1ab..2b223036 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -31,13 +31,7 @@ import tkinter as tk from platform import system from textwrap import dedent from threading import Lock -from typing import ( - TYPE_CHECKING, - Any, - Iterator, - Mapping, - MutableMapping, -) +from typing import Any, Iterator, Mapping, MutableMapping import requests import companion import edmc_data @@ -52,10 +46,7 @@ from myNotebook import Frame from prefs import prefsVersion from ttkHyperlinkLabel import HyperlinkLabel from util import text - -if TYPE_CHECKING: - def _(x: str) -> str: - return x +from l10n import translations as tr logger = get_main_logger() @@ -441,7 +432,7 @@ class EDDNSender: except requests.exceptions.RequestException as e: logger.debug('Failed sending', exc_info=e) # LANG: Error while trying to send data to EDDN - self.set_ui_status(_("Error: Can't connect to EDDN")) + self.set_ui_status(tr.tl("Error: Can't connect to EDDN")) except Exception as e: logger.debug('Failed sending', exc_info=e) @@ -566,17 +557,17 @@ class EDDNSender: if status_code == 429: # HTTP UPGRADE REQUIRED logger.warning('EDMC is sending schemas that are too old') # LANG: EDDN has banned this version of our client - return _('EDDN Error: EDMC is too old for EDDN. Please update.') + return tr.tl('EDDN Error: EDMC is too old for EDDN. Please update.') if status_code == 400: # we a validation check or something else. logger.warning(f'EDDN Error: {status_code} -- {exception.response}') # LANG: EDDN returned an error that indicates something about what we sent it was wrong - return _('EDDN Error: Validation Failed (EDMC Too Old?). See Log') + return tr.tl('EDDN Error: Validation Failed (EDMC Too Old?). See Log') logger.warning(f'Unknown status code from EDDN: {status_code} -- {exception.response}') # LANG: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number - return _('EDDN Error: Returned {STATUS} status code').format(STATUS=status_code) + return tr.tl('EDDN Error: Returned {STATUS} status code').format(STATUS=status_code) # TODO: a good few of these methods are static or could be classmethods. they should be created as such. @@ -2189,7 +2180,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_station_button = nb.Checkbutton( eddnframe, # LANG: Enable EDDN support for station data checkbox label - text=_('Send station data to the Elite Dangerous Data Network'), + text=tr.tl('Send station data to the Elite Dangerous Data Network'), variable=this.eddn_station, command=prefsvarchanged ) # Output setting @@ -2201,7 +2192,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_system_button = nb.Checkbutton( eddnframe, # LANG: Enable EDDN support for system and other scan data checkbox label - text=_('Send system and scan data to the Elite Dangerous Data Network'), + text=tr.tl('Send system and scan data to the Elite Dangerous Data Network'), variable=this.eddn_system, command=prefsvarchanged ) @@ -2213,7 +2204,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_delay_button = nb.Checkbutton( eddnframe, # LANG: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this - text=_('Delay sending until docked'), + text=tr.tl('Delay sending until docked'), variable=this.eddn_delay ) this.eddn_delay_button.grid(row=cur_row, padx=BUTTONX, pady=PADY, sticky=tk.W) @@ -2328,7 +2319,7 @@ def journal_entry( # noqa: C901, CCR001 """ should_return, new_data = killswitch.check_killswitch('plugins.eddn.journal', entry) if should_return: - plug.show_error(_('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN + plug.show_error(tr.tl('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN return None should_return, new_data = killswitch.check_killswitch(f'plugins.eddn.journal.event.{entry["event"]}', new_data) @@ -2530,7 +2521,7 @@ def journal_entry( # noqa: C901, CCR001 except requests.exceptions.RequestException as e: logger.debug('Failed in send_message', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug('Failed in export_journal_generic', exc_info=e) @@ -2568,7 +2559,7 @@ def journal_entry( # noqa: C901, CCR001 except requests.exceptions.RequestException as e: logger.debug(f'Failed exporting {entry["event"]}', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug(f'Failed exporting {entry["event"]}', exc_info=e) @@ -2622,7 +2613,8 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 status = this.parent.nametowidget(f".{appname.lower()}.status") old_status = status['text'] if not old_status: - status['text'] = _('Sending data to EDDN...') # LANG: Status text shown while attempting to send data + # LANG: Status text shown while attempting to send data + status['text'] = tr.tl('Sending data to EDDN...') status.update_idletasks() this.eddn.export_commodities(data, is_beta) @@ -2634,7 +2626,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 except requests.RequestException as e: logger.debug('Failed exporting data', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug('Failed exporting data', exc_info=e) diff --git a/plugins/edsm.py b/plugins/edsm.py index b2a8035f..5cd974d5 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -28,7 +28,7 @@ from queue import Queue from threading import Thread from time import sleep from tkinter import ttk -from typing import TYPE_CHECKING, Any, Literal, Mapping, MutableMapping, cast, Sequence +from typing import Any, Literal, Mapping, MutableMapping, cast, Sequence import requests import killswitch import monitor @@ -39,10 +39,8 @@ from config import applongname, appname, appversion, config, debug_senders, user from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel +from l10n import translations as tr -if TYPE_CHECKING: - def _(x: str) -> str: - return x # TODO: # 1) Re-factor EDSM API calls out of journal_entry() into own function. @@ -313,7 +311,8 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr this.log = tk.IntVar(value=config.get_int('edsm_out') and 1) this.log_button = nb.Checkbutton( frame, - text=_('Send flight log and CMDR status to EDSM'), # LANG: Settings>EDSM - Label on checkbox for 'send data' + # LANG: Settings>EDSM - Label on checkbox for 'send data' + text=tr.tl('Send flight log and CMDR status to EDSM'), variable=this.log, command=prefsvarchanged ) @@ -328,7 +327,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr this.label = HyperlinkLabel( frame, - text=_('Elite Dangerous Star Map credentials'), # LANG: Elite Dangerous Star Map credentials + text=tr.tl('Elite Dangerous Star Map credentials'), # LANG: Elite Dangerous Star Map credentials background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True @@ -336,21 +335,21 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr if this.label: this.label.grid(row=cur_row, columnspan=2, padx=PADX, pady=PADY, sticky=tk.W) cur_row += 1 - this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # LANG: Game Commander name label in EDSM settings + this.cmdr_label = nb.Label(frame, text=tr.tl('Cmdr')) # LANG: Game Commander name label in EDSM settings this.cmdr_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) this.cmdr_text = nb.Label(frame) this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.W) cur_row += 1 # LANG: EDSM Commander name label in EDSM settings - this.user_label = nb.Label(frame, text=_('Commander Name')) + this.user_label = nb.Label(frame, text=tr.tl('Commander Name')) this.user_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) this.user = nb.EntryMenu(frame) this.user.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 # LANG: EDSM API key label - this.apikey_label = nb.Label(frame, text=_('API Key')) + this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) this.apikey = nb.EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) @@ -362,7 +361,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr show_password_checkbox = nb.Checkbutton( frame, - text=_('Show API Key'), # LANG: Text EDSM Show API Key + text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key variable=show_password_var, command=toggle_password_visibility ) @@ -398,7 +397,7 @@ def prefs_cmdr_changed(cmdr: str | None, is_beta: bool) -> None: # noqa: CCR001 else: if this.cmdr_text: # LANG: We have no data on the current commander - this.cmdr_text['text'] = _('None') + this.cmdr_text['text'] = tr.tl('None') to_set: Literal['normal'] | Literal['disabled'] = tk.DISABLED if cmdr and not is_beta and this.log and this.log.get(): @@ -519,7 +518,7 @@ def journal_entry( # noqa: C901, CCR001 should_return, new_entry = killswitch.check_killswitch('plugins.edsm.journal', entry, logger) if should_return: # LANG: EDSM plugin - Journal handling disabled by killswitch - plug.show_error(_('EDSM Handler disabled. See Log.')) + plug.show_error(tr.tl('EDSM Handler disabled. See Log.')) return '' should_return, new_entry = killswitch.check_killswitch( @@ -606,7 +605,7 @@ entry: {entry!r}''' # LANG: The Inara API only accepts Live galaxy data, not Legacy galaxy data logger.info("EDSM only accepts Live galaxy data") this.legacy_galaxy_last_notified = datetime.now(timezone.utc) - return _("EDSM only accepts Live galaxy data") # LANG: EDSM - Only Live data + return tr.tl("EDSM only accepts Live galaxy data") # LANG: EDSM - Only Live data return '' @@ -778,7 +777,7 @@ def send_to_edsm( # noqa: CCR001 if msg_num // 100 == 2: logger.warning(f'EDSM\t{msg_num} {msg}\t{json.dumps(pending, separators=(",", ": "))}') # LANG: EDSM Plugin - Error message from EDSM API - plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) + plug.show_error(tr.tl('Error: EDSM {MSG}').format(MSG=msg)) else: if msg_num // 100 == 1: logger.trace_if('plugin.edsm.api', 'Overall OK') @@ -944,7 +943,7 @@ def worker() -> None: # noqa: CCR001 C901 else: # LANG: EDSM Plugin - Error connecting to EDSM API - plug.show_error(_("Error: Can't connect to EDSM")) + plug.show_error(tr.tl("Error: Can't connect to EDSM")) if entry['event'].lower() in ('shutdown', 'commander', 'fileheader'): # Game shutdown or new login, so we MUST not hang on to pending pending = [] @@ -1018,11 +1017,11 @@ def edsm_notify_system(reply: Mapping[str, Any]) -> None: if not reply: this.system_link['image'] = this._IMG_ERROR # LANG: EDSM Plugin - Error connecting to EDSM API - plug.show_error(_("Error: Can't connect to EDSM")) + plug.show_error(tr.tl("Error: Can't connect to EDSM")) elif reply['msgnum'] // 100 not in (1, 4): this.system_link['image'] = this._IMG_ERROR # LANG: EDSM Plugin - Error message from EDSM API - plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) + plug.show_error(tr.tl('Error: EDSM {MSG}').format(MSG=reply['msg'])) elif reply.get('systemCreated'): this.system_link['image'] = this._IMG_NEW else: diff --git a/plugins/inara.py b/plugins/inara.py index 810f7523..7c565ab3 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -30,7 +30,7 @@ from datetime import datetime, timedelta, timezone from operator import itemgetter from threading import Lock, Thread from tkinter import ttk -from typing import TYPE_CHECKING, Any, Callable, Deque, Mapping, NamedTuple, Sequence, cast, Union +from typing import Any, Callable, Deque, Mapping, NamedTuple, Sequence, cast, Union import requests import edmc_data import killswitch @@ -42,13 +42,10 @@ from config import applongname, appname, appversion, config, debug_senders from EDMCLogging import get_main_logger from monitor import monitor from ttkHyperlinkLabel import HyperlinkLabel +from l10n import translations as tr logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: - return x - _TIMEOUT = 20 FAKE = ('CQC', 'Training', 'Destination') # Fake systems that shouldn't be sent to Inara @@ -264,7 +261,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: this.log = tk.IntVar(value=config.get_int('inara_out') and 1) this.log_button = nb.Checkbutton( frame, - text=_('Send flight log and Cmdr status to Inara'), # LANG: Checkbox to enable INARA API Usage + text=tr.tl('Send flight log and Cmdr status to Inara'), # LANG: Checkbox to enable INARA API Usage variable=this.log, command=prefsvarchanged ) @@ -280,7 +277,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: # Section heading in settings this.label = HyperlinkLabel( frame, - text=_('Inara credentials'), # LANG: Text for INARA API keys link ( goes to https://inara.cz/settings-api ) + text=tr.tl('Inara credentials'), # LANG: Text for INARA API keys link ( goes to https://inara.cz/settings-api ) background=nb.Label().cget('background'), url='https://inara.cz/settings-api', underline=True @@ -290,7 +287,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: cur_row += 1 # LANG: Inara API key label - this.apikey_label = nb.Label(frame, text=_('API Key')) # Inara setting + this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) # Inara setting this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) this.apikey = nb.EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) @@ -301,7 +298,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: show_password_var.set(False) # Password is initially masked show_password_checkbox = nb.Checkbutton( frame, - text=_('Show API Key'), # LANG: Text Inara Show API key + text=tr.tl('Show API Key'), # LANG: Text Inara Show API key variable=show_password_var, command=toggle_password_visibility, ) @@ -407,7 +404,7 @@ def journal_entry( # noqa: C901, CCR001 should_return, new_entry = killswitch.check_killswitch('plugins.inara.journal', entry, logger) if should_return: - plug.show_error(_('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch + plug.show_error(tr.tl('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch logger.trace('returning due to killswitch match') return '' @@ -432,9 +429,9 @@ def journal_entry( # noqa: C901, CCR001 and config.get_int('inara_out') and not (is_beta or this.multicrew or credentials(cmdr)) ): # LANG: The Inara API only accepts Live galaxy data, not Legacy galaxy data - logger.info(_("Inara only accepts Live galaxy data")) + logger.info(tr.tl("Inara only accepts Live galaxy data")) this.legacy_galaxy_last_notified = datetime.now(timezone.utc) - return _("Inara only accepts Live galaxy data") # LANG: Inara - Only Live data + return tr.tl("Inara only accepts Live galaxy data") # LANG: Inara - Only Live data return '' @@ -1642,7 +1639,7 @@ def handle_api_error(data: Mapping[str, Any], status: int, reply: dict[str, Any] logger.warning(f'Inara\t{status} {error_message}') logger.debug(f'JSON data:\n{json.dumps(data, indent=2, separators = (",", ": "))}') # LANG: INARA API returned some kind of error (error message will be contained in {MSG}) - plug.show_error(_('Error: Inara {MSG}').format(MSG=error_message)) + plug.show_error(tr.tl('Error: Inara {MSG}').format(MSG=error_message)) def handle_success_reply(data: Mapping[str, Any], reply: dict[str, Any]) -> None: @@ -1675,7 +1672,7 @@ def handle_individual_error(data_event: dict[str, Any], reply_status: int, reply if reply_status // 100 != 2: # LANG: INARA API returned some kind of error (error message will be contained in {MSG}) - plug.show_error(_('Error: Inara {MSG}').format( + plug.show_error(tr.tl('Error: Inara {MSG}').format( MSG=f'{data_event["eventName"]}, {reply_text}' )) diff --git a/prefs.py b/prefs.py index 285ef0d7..2d4c8d51 100644 --- a/prefs.py +++ b/prefs.py @@ -14,23 +14,19 @@ from os.path import expanduser, expandvars, join, normpath from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812 from tkinter import ttk from types import TracebackType -from typing import TYPE_CHECKING, Any, Callable, Optional, Type - +from typing import Any, Callable, Optional, Type import myNotebook as nb # noqa: N813 import plug from config import appversion_nobuild, config from EDMCLogging import edmclogger, get_main_logger from constants import appname from hotkey import hotkeymgr -from l10n import Translations +from l10n import translations as tr from monitor import monitor from theme import theme from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: - return x # TODO: Decouple this from platform as far as possible @@ -224,7 +220,7 @@ class PreferencesDialog(tk.Toplevel): self.parent = parent self.callback = callback # LANG: File > Settings (macOS) - self.title(_('Settings')) + self.title(tr.tl('Settings')) if parent.winfo_viewable(): self.transient(parent) @@ -270,7 +266,7 @@ class PreferencesDialog(tk.Toplevel): buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer # LANG: 'OK' button on Settings/Preferences window - button = ttk.Button(buttonframe, text=_('OK'), command=self.apply) + button = ttk.Button(buttonframe, text=tr.tl('OK'), command=self.apply) button.grid(row=0, column=1, sticky=tk.E) button.bind("", lambda event: self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) @@ -313,13 +309,13 @@ class PreferencesDialog(tk.Toplevel): row = AutoInc(start=0) # LANG: Settings > Output - choosing what data to save to files - self.out_label = nb.Label(output_frame, text=_('Please choose what data to save')) + self.out_label = nb.Label(output_frame, text=tr.tl('Please choose what data to save')) self.out_label.grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.out_csv = tk.IntVar(value=1 if (output & config.OUT_MKT_CSV) else 0) self.out_csv_button = nb.Checkbutton( output_frame, - text=_('Market data in CSV format file'), # LANG: Settings > Output option + text=tr.tl('Market data in CSV format file'), # LANG: Settings > Output option variable=self.out_csv, command=self.outvarchanged ) @@ -328,7 +324,7 @@ class PreferencesDialog(tk.Toplevel): self.out_td = tk.IntVar(value=1 if (output & config.OUT_MKT_TD) else 0) self.out_td_button = nb.Checkbutton( output_frame, - text=_('Market data in Trade Dangerous format file'), # LANG: Settings > Output option + text=tr.tl('Market data in Trade Dangerous format file'), # LANG: Settings > Output option variable=self.out_td, command=self.outvarchanged ) @@ -338,7 +334,7 @@ class PreferencesDialog(tk.Toplevel): # Output setting self.out_ship_button = nb.Checkbutton( output_frame, - text=_('Ship loadout'), # LANG: Settings > Output option + text=tr.tl('Ship loadout'), # LANG: Settings > Output option variable=self.out_ship, command=self.outvarchanged ) @@ -348,7 +344,7 @@ class PreferencesDialog(tk.Toplevel): # Output setting self.out_auto_button = nb.Checkbutton( output_frame, - text=_('Automatically update on docking'), # LANG: Settings > Output option + text=tr.tl('Automatically update on docking'), # LANG: Settings > Output option variable=self.out_auto, command=self.outvarchanged ) @@ -357,14 +353,14 @@ class PreferencesDialog(tk.Toplevel): self.outdir = tk.StringVar() self.outdir.set(str(config.get_str('outdir'))) # LANG: Settings > Output - Label for "where files are located" - self.outdir_label = nb.Label(output_frame, text=_('File location')+':') # Section heading in settings + self.outdir_label = nb.Label(output_frame, text=tr.tl('File location')+':') # Section heading in settings # Type ignored due to incorrect type annotation. a 2 tuple does padding for each side self.outdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) # type: ignore self.outdir_entry = ttk.Entry(output_frame, takefocus=False) self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) - text = _('Browse...') # LANG: NOT-macOS Settings - files location selection button + text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button self.outbutton = ttk.Button( output_frame, @@ -372,12 +368,12 @@ class PreferencesDialog(tk.Toplevel): # Technically this is different from the label in Settings > Output, as *this* is used # as the title of the popup folder selection window. # LANG: Settings > Output - Label for "where files are located" - command=lambda: self.filebrowse(_('File location'), self.outdir) + command=lambda: self.filebrowse(tr.tl('File location'), self.outdir) ) self.outbutton.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=row.get()) # LANG: Label for 'Output' Settings/Preferences tab - root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings + root_notebook.add(output_frame, text=tr.tl('Output')) # Tab heading in settings def __setup_plugin_tabs(self, notebook: ttk.Notebook) -> None: for plugin in plug.PLUGINS: @@ -403,19 +399,19 @@ class PreferencesDialog(tk.Toplevel): nb.Label( config_frame, # LANG: Settings > Configuration - Label for Journal files location - text=_('E:D journal file location')+':' + text=tr.tl('E:D journal file location')+':' ).grid(columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) - text = _('Browse...') # LANG: NOT-macOS Setting - files location selection button + text = tr.tl('Browse...') # LANG: NOT-macOS Setting - files location selection button with row as cur_row: self.logbutton = ttk.Button( config_frame, text=text, # LANG: Settings > Configuration - Label for Journal files location - command=lambda: self.filebrowse(_('E:D journal file location'), self.logdir) + command=lambda: self.filebrowse(tr.tl('E:D journal file location'), self.logdir) ) self.logbutton.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) @@ -424,7 +420,7 @@ class PreferencesDialog(tk.Toplevel): ttk.Button( config_frame, # LANG: Settings > Configuration - Label on 'reset journal files location to default' button - text=_('Default'), + text=tr.tl('Default'), command=self.logdir_reset, state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED ).grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) @@ -438,13 +434,13 @@ class PreferencesDialog(tk.Toplevel): nb.Label( config_frame, - text=_('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section + text=tr.tl('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) nb.Checkbutton( config_frame, # LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls - text=_('Enable Fleetcarrier CAPI Queries'), + text=tr.tl('Enable Fleetcarrier CAPI Queries'), variable=self.capi_fleetcarrier ).grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) @@ -460,7 +456,7 @@ class PreferencesDialog(tk.Toplevel): with row as cur_row: nb.Label( config_frame, - text=_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows + text=tr.tl('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) self.hotkey_text = ttk.Entry(config_frame, width=30, justify=tk.CENTER) @@ -469,7 +465,7 @@ class PreferencesDialog(tk.Toplevel): # No hotkey/shortcut currently defined # TODO: display Only shows up on windows # LANG: No hotkey/shortcut set - hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None') + hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None') ) self.hotkey_text.bind('', self.hotkeystart) @@ -480,7 +476,7 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_only_btn = nb.Checkbutton( config_frame, # LANG: Configuration - Act on hotkey only when ED is in foreground - text=_('Only when Elite: Dangerous is the active app'), + text=tr.tl('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state=tk.NORMAL if self.hotkey_code else tk.DISABLED ) @@ -491,7 +487,7 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_play_btn = nb.Checkbutton( config_frame, # LANG: Configuration - play sound when hotkey used - text=_('Play sound'), + text=tr.tl('Play sound'), variable=self.hotkey_play, state=tk.NORMAL if self.hotkey_code else tk.DISABLED ) @@ -506,7 +502,7 @@ class PreferencesDialog(tk.Toplevel): self.disable_autoappupdatecheckingame_btn = nb.Checkbutton( config_frame, # LANG: Configuration - disable checks for app updates when in-game - text=_('Disable Automatic Application Updates Check when in-game'), + text=tr.tl('Disable Automatic Application Updates Check when in-game'), variable=self.disable_autoappupdatecheckingame, command=self.disable_autoappupdatecheckingame_changed ) @@ -521,7 +517,7 @@ class PreferencesDialog(tk.Toplevel): # Settings prompt for preferred ship loadout, system and station info websites # LANG: Label for preferred shipyard, system and station 'providers' - nb.Label(config_frame, text=_('Preferred websites')).grid( + nb.Label(config_frame, text=tr.tl('Preferred websites')).grid( columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -532,7 +528,9 @@ class PreferencesDialog(tk.Toplevel): ) # Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis # LANG: Label for Shipyard provider selection - nb.Label(config_frame, text=_('Shipyard')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('Shipyard')).grid( + padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row + ) self.shipyard_button = nb.OptionMenu( config_frame, self.shipyard_provider, self.shipyard_provider.get(), *plug.provides('shipyard_url') ) @@ -544,7 +542,7 @@ class PreferencesDialog(tk.Toplevel): self.alt_shipyard_open_btn = nb.Checkbutton( config_frame, # LANG: Label for checkbox to utilise alternative Coriolis URL method - text=_('Use alternate URL method'), + text=tr.tl('Use alternate URL method'), variable=self.alt_shipyard_open, command=self.alt_shipyard_open_changed, ) @@ -558,7 +556,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Configuration - Label for selection of 'System' provider website - nb.Label(config_frame, text=_('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) self.system_button = nb.OptionMenu( config_frame, self.system_provider, @@ -576,7 +574,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Configuration - Label for selection of 'Station' provider website - nb.Label(config_frame, text=_('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) self.station_button = nb.OptionMenu( config_frame, self.station_provider, @@ -597,7 +595,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( config_frame, # LANG: Configuration - Label for selection of Log Level - text=_('Log Level') + text=tr.tl('Log Level') ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) current_loglevel = config.get_str('loglevel') @@ -624,7 +622,7 @@ class PreferencesDialog(tk.Toplevel): ttk.Button( config_frame, # LANG: Label on button used to open a filesystem folder - text=_('Open Log Folder'), # Button that opens a folder in Explorer/Finder + text=tr.tl('Open Log Folder'), # Button that opens a folder in Explorer/Finder command=lambda: help_open_log_folder() ).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row) @@ -632,7 +630,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label(config_frame).grid(sticky=tk.W, row=row.get()) # LANG: Label for 'Configuration' tab in Settings - notebook.add(config_frame, text=_('Configuration')) + notebook.add(config_frame, text=tr.tl('Configuration')) def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None: privacy_frame = nb.Frame(notebook) @@ -641,37 +639,37 @@ class PreferencesDialog(tk.Toplevel): row = AutoInc(start=0) # LANG: UI elements privacy section header in privacy tab of preferences - nb.Label(privacy_frame, text=_('Main UI privacy options')).grid( + nb.Label(privacy_frame, text=tr.tl('Main UI privacy options')).grid( row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=self.PADY ) nb.Checkbutton( # LANG: Hide private group owner name from UI checkbox - privacy_frame, text=_('Hide private group name in UI'), + privacy_frame, text=tr.tl('Hide private group name in UI'), variable=self.hide_private_group ).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W) nb.Checkbutton( # LANG: Hide multicrew captain name from main UI checkbox - privacy_frame, text=_('Hide multi-crew captain name'), + privacy_frame, text=tr.tl('Hide multi-crew captain name'), variable=self.hide_multicrew_captain ).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W) - notebook.add(privacy_frame, text=_('Privacy')) # LANG: Preferences privacy tab title + notebook.add(privacy_frame, text=tr.tl('Privacy')) # LANG: Preferences privacy tab title def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: - self.languages = Translations.available_names() + self.languages = tr.available_names() # Appearance theme and language setting # LANG: The system default language choice in Settings > Appearance - self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), _('Default'))) + self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), tr.tl('Default'))) self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) self.minimize_system_tray = tk.BooleanVar(value=config.get_bool('minimize_system_tray')) self.theme = tk.IntVar(value=config.get_int('theme')) self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')] self.theme_prompts = [ # LANG: Label for Settings > Appeareance > selection of 'normal' text colour - _('Normal text'), # Dark theme color setting + tr.tl('Normal text'), # Dark theme color setting # LANG: Label for Settings > Appeareance > selection of 'highlightes' text colour - _('Highlighted text'), # Dark theme color setting + tr.tl('Highlighted text'), # Dark theme color setting ] row = AutoInc(start=0) @@ -680,7 +678,7 @@ class PreferencesDialog(tk.Toplevel): appearance_frame.columnconfigure(2, weight=1) with row as cur_row: # LANG: Appearance - Label for selection of application display language - nb.Label(appearance_frame, text=_('Language')).grid( + nb.Label(appearance_frame, text=tr.tl('Language')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) self.lang_button = nb.OptionMenu(appearance_frame, self.lang, self.lang.get(), *self.languages.values()) @@ -692,28 +690,29 @@ class PreferencesDialog(tk.Toplevel): # Appearance setting # LANG: Label for Settings > Appearance > Theme selection - nb.Label(appearance_frame, text=_('Theme')).grid( + nb.Label(appearance_frame, text=tr.tl('Theme')).grid( columnspan=3, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) # Appearance theme and language setting nb.Radiobutton( # LANG: Label for 'Default' theme radio button - appearance_frame, text=_('Default'), variable=self.theme, + appearance_frame, text=tr.tl('Default'), variable=self.theme, value=theme.THEME_DEFAULT, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance theme setting nb.Radiobutton( # LANG: Label for 'Dark' theme radio button - appearance_frame, text=_('Dark'), variable=self.theme, value=theme.THEME_DARK, command=self.themevarchanged + appearance_frame, text=tr.tl('Dark'), variable=self.theme, + value=theme.THEME_DARK, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) if sys.platform == 'win32': nb.Radiobutton( appearance_frame, # LANG: Label for 'Transparent' theme radio button - text=_('Transparent'), # Appearance theme setting + text=tr.tl('Transparent'), # Appearance theme setting variable=self.theme, value=theme.THEME_TRANSPARENT, command=self.themevarchanged @@ -727,7 +726,7 @@ class PreferencesDialog(tk.Toplevel): self.theme_button_0 = tk.Button( appearance_frame, # LANG: Appearance - Example 'Normal' text - text=_('Station'), + text=tr.tl('Station'), background='grey4', command=lambda: self.themecolorbrowse(0) ) @@ -759,7 +758,7 @@ class PreferencesDialog(tk.Toplevel): ) with row as cur_row: # LANG: Appearance - Label for selection of UI scaling - nb.Label(appearance_frame, text=_('UI Scale Percentage')).grid( + nb.Label(appearance_frame, text=tr.tl('UI Scale Percentage')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) @@ -780,7 +779,7 @@ class PreferencesDialog(tk.Toplevel): self.ui_scaling_defaultis = nb.Label( appearance_frame, # LANG: Appearance - Help/hint text for UI scaling selection - text=_('100 means Default{CR}Restart Required for{CR}changes to take effect!') + text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!') ).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row) # Transparency slider @@ -790,7 +789,7 @@ class PreferencesDialog(tk.Toplevel): with row as cur_row: # LANG: Appearance - Label for selection of main window transparency - nb.Label(appearance_frame, text=_("Main window transparency")).grid( + nb.Label(appearance_frame, text=tr.tl("Main window transparency")).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) self.transparency = tk.IntVar() @@ -810,7 +809,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( appearance_frame, # LANG: Appearance - Help/hint text for Main window transparency selection - text=_( + text=tr.tl( "100 means fully opaque.{CR}" "Window is updated in real time" ).format(CR='\n') @@ -832,7 +831,7 @@ class PreferencesDialog(tk.Toplevel): self.ontop_button = nb.Checkbutton( appearance_frame, # LANG: Appearance - Label for checkbox to select if application always on top - text=_('Always on top'), + text=tr.tl('Always on top'), variable=self.always_ontop, command=self.themevarchanged ) @@ -844,7 +843,7 @@ class PreferencesDialog(tk.Toplevel): nb.Checkbutton( appearance_frame, # LANG: Appearance option for Windows "minimize to system tray" - text=_('Minimize to system tray'), + text=tr.tl('Minimize to system tray'), variable=self.minimize_system_tray, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance setting @@ -852,7 +851,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer # LANG: Label for Settings > Appearance tab - notebook.add(appearance_frame, text=_('Appearance')) # Tab heading in settings + notebook.add(appearance_frame, text=tr.tl('Appearance')) # Tab heading in settings def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 # Plugin settings and info @@ -864,7 +863,7 @@ class PreferencesDialog(tk.Toplevel): # Section heading in settings # LANG: Label for location of third-party plugins folder - nb.Label(plugins_frame, text=_('Plugins folder') + ':').grid( + nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -877,13 +876,14 @@ class PreferencesDialog(tk.Toplevel): plugins_frame, # Help text in settings # LANG: Tip/label about how to disable plugins - text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled') + text=tr.tl( + "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled') ).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) ttk.Button( plugins_frame, # LANG: Label on button used to open a filesystem folder - text=_('Open'), # Button that opens a folder in Explorer/Finder + text=tr.tl('Open'), # Button that opens a folder in Explorer/Finder command=lambda: webbrowser.open(f'file:///{config.plugin_dir_path}') ).grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row) @@ -895,7 +895,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( plugins_frame, # LANG: Label on list of enabled plugins - text=_('Enabled Plugins')+':' # List of plugins in settings + text=tr.tl('Enabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in enabled_plugins: @@ -915,13 +915,13 @@ class PreferencesDialog(tk.Toplevel): columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x - nb.Label(plugins_frame, text=_('Plugins Without Python 3.x Support')+':').grid( + nb.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) HyperlinkLabel( # LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7 - plugins_frame, text=_('Information on migrating plugins'), + plugins_frame, text=tr.tl('Information on migrating plugins'), background=nb.Label().cget('background'), url='https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#migration-from-python-27', underline=True @@ -943,7 +943,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( plugins_frame, # LANG: Label on list of user-disabled plugins - text=_('Disabled Plugins')+':' # List of plugins in settings + text=tr.tl('Disabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in disabled_plugins: @@ -958,7 +958,7 @@ class PreferencesDialog(tk.Toplevel): columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'broken' plugins that failed to load - nb.Label(plugins_frame, text=_('Broken Plugins')+':').grid( + nb.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -969,7 +969,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Label on Settings > Plugins tab - notebook.add(plugins_frame, text=_('Plugins')) # Tab heading in settings + notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings def cmdrchanged(self, event=None): """ @@ -1122,7 +1122,7 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_text.insert( 0, # LANG: No hotkey/shortcut set - hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')) + hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None')) def hotkeylisten(self, event: 'tk.Event[Any]') -> str: """ @@ -1155,7 +1155,7 @@ class PreferencesDialog(tk.Toplevel): else: # LANG: No hotkey/shortcut set - event.widget.insert(0, _('None')) + event.widget.insert(0, tr.tl('None')) self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED @@ -1205,7 +1205,7 @@ class PreferencesDialog(tk.Toplevel): lang_codes = {v: k for k, v in self.languages.items()} # Codes by name config.set('language', lang_codes.get(self.lang.get()) or '') # or '' used here due to Default being None above - Translations.install(config.get_str('language', default=None)) # type: ignore # This sets self in weird ways. + tr.install(config.get_str('language', default=None)) # type: ignore # This sets self in weird ways. # Privacy options config.set('hide_private_group', self.hide_private_group.get()) diff --git a/stats.py b/stats.py index c377e5d3..db61f789 100644 --- a/stats.py +++ b/stats.py @@ -12,20 +12,17 @@ import json import sys import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any, AnyStr, Callable, NamedTuple, Sequence, cast +from typing import Any, AnyStr, Callable, NamedTuple, Sequence, cast import companion import EDMCLogging import myNotebook as nb # noqa: N813 from edmc_data import ship_name_map from hotkey import hotkeymgr -from l10n import Locale +from l10n import Locale, translations as tr from monitor import monitor logger = EDMCLogging.get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: return x - if sys.platform == 'win32': import ctypes from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT @@ -60,32 +57,32 @@ def status(data: dict[str, Any]) -> list[list[str]]: """ # StatsResults assumes these three things are first res = [ - [_('Cmdr'), data['commander']['name']], # LANG: Cmdr stats - [_('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats - [_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats + [tr.tl('Cmdr'), data['commander']['name']], # LANG: Cmdr stats + [tr.tl('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats + [tr.tl('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats ] _ELITE_RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime - _('Elite'), # LANG: Top rank - _('Elite I'), # LANG: Top rank +1 - _('Elite II'), # LANG: Top rank +2 - _('Elite III'), # LANG: Top rank +3 - _('Elite IV'), # LANG: Top rank +4 - _('Elite V'), # LANG: Top rank +5 + tr.tl('Elite'), # LANG: Top rank + tr.tl('Elite I'), # LANG: Top rank +1 + tr.tl('Elite II'), # LANG: Top rank +2 + tr.tl('Elite III'), # LANG: Top rank +3 + tr.tl('Elite IV'), # LANG: Top rank +4 + tr.tl('Elite V'), # LANG: Top rank +5 ] RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime # in output order # Names we show people, vs internal names - (_('Combat'), 'combat'), # LANG: Ranking - (_('Trade'), 'trade'), # LANG: Ranking - (_('Explorer'), 'explore'), # LANG: Ranking - (_('Mercenary'), 'soldier'), # LANG: Ranking - (_('Exobiologist'), 'exobiologist'), # LANG: Ranking - (_('CQC'), 'cqc'), # LANG: Ranking - (_('Federation'), 'federation'), # LANG: Ranking - (_('Empire'), 'empire'), # LANG: Ranking - (_('Powerplay'), 'power'), # LANG: Ranking + (tr.tl('Combat'), 'combat'), # LANG: Ranking + (tr.tl('Trade'), 'trade'), # LANG: Ranking + (tr.tl('Explorer'), 'explore'), # LANG: Ranking + (tr.tl('Mercenary'), 'soldier'), # LANG: Ranking + (tr.tl('Exobiologist'), 'exobiologist'), # LANG: Ranking + (tr.tl('CQC'), 'cqc'), # LANG: Ranking + (tr.tl('Federation'), 'federation'), # LANG: Ranking + (tr.tl('Empire'), 'empire'), # LANG: Ranking + (tr.tl('Powerplay'), 'power'), # LANG: Ranking # ??? , 'crime'), # LANG: Ranking # ??? , 'service'), # LANG: Ranking ] @@ -94,113 +91,113 @@ def status(data: dict[str, Any]) -> list[list[str]]: # These names are the fdev side name (but lower()ed) # http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks 'combat': [ - _('Harmless'), # LANG: Combat rank - _('Mostly Harmless'), # LANG: Combat rank - _('Novice'), # LANG: Combat rank - _('Competent'), # LANG: Combat rank - _('Expert'), # LANG: Combat rank - _('Master'), # LANG: Combat rank - _('Dangerous'), # LANG: Combat rank - _('Deadly'), # LANG: Combat rank + tr.tl('Harmless'), # LANG: Combat rank + tr.tl('Mostly Harmless'), # LANG: Combat rank + tr.tl('Novice'), # LANG: Combat rank + tr.tl('Competent'), # LANG: Combat rank + tr.tl('Expert'), # LANG: Combat rank + tr.tl('Master'), # LANG: Combat rank + tr.tl('Dangerous'), # LANG: Combat rank + tr.tl('Deadly'), # LANG: Combat rank ] + _ELITE_RANKS, 'trade': [ - _('Penniless'), # LANG: Trade rank - _('Mostly Penniless'), # LANG: Trade rank - _('Peddler'), # LANG: Trade rank - _('Dealer'), # LANG: Trade rank - _('Merchant'), # LANG: Trade rank - _('Broker'), # LANG: Trade rank - _('Entrepreneur'), # LANG: Trade rank - _('Tycoon'), # LANG: Trade rank + tr.tl('Penniless'), # LANG: Trade rank + tr.tl('Mostly Penniless'), # LANG: Trade rank + tr.tl('Peddler'), # LANG: Trade rank + tr.tl('Dealer'), # LANG: Trade rank + tr.tl('Merchant'), # LANG: Trade rank + tr.tl('Broker'), # LANG: Trade rank + tr.tl('Entrepreneur'), # LANG: Trade rank + tr.tl('Tycoon'), # LANG: Trade rank ] + _ELITE_RANKS, 'explore': [ - _('Aimless'), # LANG: Explorer rank - _('Mostly Aimless'), # LANG: Explorer rank - _('Scout'), # LANG: Explorer rank - _('Surveyor'), # LANG: Explorer rank - _('Trailblazer'), # LANG: Explorer rank - _('Pathfinder'), # LANG: Explorer rank - _('Ranger'), # LANG: Explorer rank - _('Pioneer'), # LANG: Explorer rank + tr.tl('Aimless'), # LANG: Explorer rank + tr.tl('Mostly Aimless'), # LANG: Explorer rank + tr.tl('Scout'), # LANG: Explorer rank + tr.tl('Surveyor'), # LANG: Explorer rank + tr.tl('Trailblazer'), # LANG: Explorer rank + tr.tl('Pathfinder'), # LANG: Explorer rank + tr.tl('Ranger'), # LANG: Explorer rank + tr.tl('Pioneer'), # LANG: Explorer rank ] + _ELITE_RANKS, 'soldier': [ - _('Defenceless'), # LANG: Mercenary rank - _('Mostly Defenceless'), # LANG: Mercenary rank - _('Rookie'), # LANG: Mercenary rank - _('Soldier'), # LANG: Mercenary rank - _('Gunslinger'), # LANG: Mercenary rank - _('Warrior'), # LANG: Mercenary rank - _('Gunslinger'), # LANG: Mercenary rank - _('Deadeye'), # LANG: Mercenary rank + tr.tl('Defenceless'), # LANG: Mercenary rank + tr.tl('Mostly Defenceless'), # LANG: Mercenary rank + tr.tl('Rookie'), # LANG: Mercenary rank + tr.tl('Soldier'), # LANG: Mercenary rank + tr.tl('Gunslinger'), # LANG: Mercenary rank + tr.tl('Warrior'), # LANG: Mercenary rank + tr.tl('Gunslinger'), # LANG: Mercenary rank + tr.tl('Deadeye'), # LANG: Mercenary rank ] + _ELITE_RANKS, 'exobiologist': [ - _('Directionless'), # LANG: Exobiologist rank - _('Mostly Directionless'), # LANG: Exobiologist rank - _('Compiler'), # LANG: Exobiologist rank - _('Collector'), # LANG: Exobiologist rank - _('Cataloguer'), # LANG: Exobiologist rank - _('Taxonomist'), # LANG: Exobiologist rank - _('Ecologist'), # LANG: Exobiologist rank - _('Geneticist'), # LANG: Exobiologist rank + tr.tl('Directionless'), # LANG: Exobiologist rank + tr.tl('Mostly Directionless'), # LANG: Exobiologist rank + tr.tl('Compiler'), # LANG: Exobiologist rank + tr.tl('Collector'), # LANG: Exobiologist rank + tr.tl('Cataloguer'), # LANG: Exobiologist rank + tr.tl('Taxonomist'), # LANG: Exobiologist rank + tr.tl('Ecologist'), # LANG: Exobiologist rank + tr.tl('Geneticist'), # LANG: Exobiologist rank ] + _ELITE_RANKS, 'cqc': [ - _('Helpless'), # LANG: CQC rank - _('Mostly Helpless'), # LANG: CQC rank - _('Amateur'), # LANG: CQC rank - _('Semi Professional'), # LANG: CQC rank - _('Professional'), # LANG: CQC rank - _('Champion'), # LANG: CQC rank - _('Hero'), # LANG: CQC rank - _('Gladiator'), # LANG: CQC rank + tr.tl('Helpless'), # LANG: CQC rank + tr.tl('Mostly Helpless'), # LANG: CQC rank + tr.tl('Amateur'), # LANG: CQC rank + tr.tl('Semi Professional'), # LANG: CQC rank + tr.tl('Professional'), # LANG: CQC rank + tr.tl('Champion'), # LANG: CQC rank + tr.tl('Hero'), # LANG: CQC rank + tr.tl('Gladiator'), # LANG: CQC rank ] + _ELITE_RANKS, # http://elite-dangerous.wikia.com/wiki/Federation#Ranks 'federation': [ - _('None'), # LANG: No rank - _('Recruit'), # LANG: Federation rank - _('Cadet'), # LANG: Federation rank - _('Midshipman'), # LANG: Federation rank - _('Petty Officer'), # LANG: Federation rank - _('Chief Petty Officer'), # LANG: Federation rank - _('Warrant Officer'), # LANG: Federation rank - _('Ensign'), # LANG: Federation rank - _('Lieutenant'), # LANG: Federation rank - _('Lieutenant Commander'), # LANG: Federation rank - _('Post Commander'), # LANG: Federation rank - _('Post Captain'), # LANG: Federation rank - _('Rear Admiral'), # LANG: Federation rank - _('Vice Admiral'), # LANG: Federation rank - _('Admiral') # LANG: Federation rank + tr.tl('None'), # LANG: No rank + tr.tl('Recruit'), # LANG: Federation rank + tr.tl('Cadet'), # LANG: Federation rank + tr.tl('Midshipman'), # LANG: Federation rank + tr.tl('Petty Officer'), # LANG: Federation rank + tr.tl('Chief Petty Officer'), # LANG: Federation rank + tr.tl('Warrant Officer'), # LANG: Federation rank + tr.tl('Ensign'), # LANG: Federation rank + tr.tl('Lieutenant'), # LANG: Federation rank + tr.tl('Lieutenant Commander'), # LANG: Federation rank + tr.tl('Post Commander'), # LANG: Federation rank + tr.tl('Post Captain'), # LANG: Federation rank + tr.tl('Rear Admiral'), # LANG: Federation rank + tr.tl('Vice Admiral'), # LANG: Federation rank + tr.tl('Admiral') # LANG: Federation rank ], # http://elite-dangerous.wikia.com/wiki/Empire#Ranks 'empire': [ - _('None'), # LANG: No rank - _('Outsider'), # LANG: Empire rank - _('Serf'), # LANG: Empire rank - _('Master'), # LANG: Empire rank - _('Squire'), # LANG: Empire rank - _('Knight'), # LANG: Empire rank - _('Lord'), # LANG: Empire rank - _('Baron'), # LANG: Empire rank - _('Viscount'), # LANG: Empire rank - _('Count'), # LANG: Empire rank - _('Earl'), # LANG: Empire rank - _('Marquis'), # LANG: Empire rank - _('Duke'), # LANG: Empire rank - _('Prince'), # LANG: Empire rank - _('King') # LANG: Empire rank + tr.tl('None'), # LANG: No rank + tr.tl('Outsider'), # LANG: Empire rank + tr.tl('Serf'), # LANG: Empire rank + tr.tl('Master'), # LANG: Empire rank + tr.tl('Squire'), # LANG: Empire rank + tr.tl('Knight'), # LANG: Empire rank + tr.tl('Lord'), # LANG: Empire rank + tr.tl('Baron'), # LANG: Empire rank + tr.tl('Viscount'), # LANG: Empire rank + tr.tl('Count'), # LANG: Empire rank + tr.tl('Earl'), # LANG: Empire rank + tr.tl('Marquis'), # LANG: Empire rank + tr.tl('Duke'), # LANG: Empire rank + tr.tl('Prince'), # LANG: Empire rank + tr.tl('King') # LANG: Empire rank ], # http://elite-dangerous.wikia.com/wiki/Ratings 'power': [ - _('None'), # LANG: No rank - _('Rating 1'), # LANG: Power rank - _('Rating 2'), # LANG: Power rank - _('Rating 3'), # LANG: Power rank - _('Rating 4'), # LANG: Power rank - _('Rating 5') # LANG: Power rank + tr.tl('None'), # LANG: No rank + tr.tl('Rating 1'), # LANG: Power rank + tr.tl('Rating 2'), # LANG: Power rank + tr.tl('Rating 3'), # LANG: Power rank + tr.tl('Rating 4'), # LANG: Power rank + tr.tl('Rating 5') # LANG: Power rank ], } @@ -212,7 +209,7 @@ def status(data: dict[str, Any]) -> list[list[str]]: res.append([title, names[rank] if rank < len(names) else f'Rank {rank}']) else: - res.append([title, _('None')]) # LANG: No rank + res.append([title, tr.tl('None')]) # LANG: No rank return res @@ -318,7 +315,7 @@ class StatsDialog(): if not monitor.cmdr: hotkeymgr.play_bad() # LANG: Current commander unknown when trying to use 'File' > 'Status' - self.status['text'] = _("Status: Don't yet know your Commander name") + self.status['text'] = tr.tl("Status: Don't yet know your Commander name") return # TODO: This needs to use cached data @@ -326,7 +323,7 @@ class StatsDialog(): logger.info('No cached data, aborting...') hotkeymgr.play_bad() # LANG: No Frontier CAPI data yet when trying to use 'File' > 'Status' - self.status['text'] = _("Status: No CAPI data yet") + self.status['text'] = tr.tl("Status: No CAPI data yet") return capi_data = json.loads( @@ -336,7 +333,7 @@ class StatsDialog(): if not capi_data.get('commander') or not capi_data['commander'].get('name', '').strip(): # Shouldn't happen # LANG: Unknown commander - self.status['text'] = _("Who are you?!") + self.status['text'] = tr.tl("Who are you?!") elif ( not capi_data.get('lastSystem') @@ -344,7 +341,7 @@ class StatsDialog(): ): # Shouldn't happen # LANG: Unknown location - self.status['text'] = _("Where are you?!") + self.status['text'] = tr.tl("Where are you?!") elif ( not capi_data.get('ship') or not capi_data['ship'].get('modules') @@ -352,7 +349,7 @@ class StatsDialog(): ): # Shouldn't happen # LANG: Unknown ship - self.status['text'] = _("What are you flying?!") + self.status['text'] = tr.tl("What are you flying?!") else: self.status['text'] = '' @@ -401,14 +398,14 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, thing, with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Status')) # LANG: Status dialog title + notebook.add(page, text=tr.tl('Status')) # LANG: Status dialog title page = self.addpage(notebook, [ - _('Ship'), # LANG: Status dialog subtitle + tr.tl('Ship'), # LANG: Status dialog subtitle '', - _('System'), # LANG: Main window - _('Station'), # LANG: Status dialog subtitle - _('Value'), # LANG: Status dialog subtitle - CR value of ship + tr.tl('System'), # LANG: Main window + tr.tl('Station'), # LANG: Status dialog subtitle + tr.tl('Value'), # LANG: Status dialog subtitle - CR value of ship ]) shiplist = ships(data) @@ -417,7 +414,7 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, list(ship_data[1:-1]) + [self.credits(int(ship_data[-1]))], with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Ships')) # LANG: Status dialog title + notebook.add(page, text=tr.tl('Ships')) # LANG: Status dialog title # wait for window to appear on screen before calling grab_set self.wait_visibility() diff --git a/theme.py b/theme.py index bbe62ef5..94e99f7a 100644 --- a/theme.py +++ b/theme.py @@ -16,17 +16,14 @@ import tkinter as tk from os.path import join from tkinter import font as tk_font from tkinter import ttk -from typing import TYPE_CHECKING, Callable - +from typing import Callable +from l10n import translations as tr from config import config from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: ... - if __debug__: from traceback import print_exc @@ -291,7 +288,7 @@ class _Theme: # Font only supports Latin 1 / Supplement / Extended, and a # few General Punctuation and Mathematical Operators # LANG: Label for commander name in main window - 'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and + 'font': (theme > 1 and not 0x250 < ord(tr.tl('Cmdr')[0]) < 0x3000 and tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or 'TkDefaultFont'), } diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 9bb3b9bf..d026f619 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -25,10 +25,8 @@ import tkinter as tk import webbrowser from tkinter import font as tk_font from tkinter import ttk -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - def _(x: str) -> str: return x +from typing import Any +from l10n import translations as tr class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore @@ -55,7 +53,7 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore self.menu = tk.Menu(tearoff=tk.FALSE) # LANG: Label for 'Copy' as in 'Copy and Paste' - self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste + self.menu.add_command(label=tr.tl('Copy'), command=self.copy) # As in Copy and Paste self.bind('', self._contextmenu) self.bind('', self._enter) diff --git a/update.py b/update.py index 991558d6..52983a5d 100644 --- a/update.py +++ b/update.py @@ -16,10 +16,10 @@ import requests import semantic_version from config import appname, appversion_nobuild, config, update_feed from EDMCLogging import get_main_logger +from l10n import translations as tr if TYPE_CHECKING: import tkinter as tk - def _(x: str): return x logger = get_main_logger() @@ -200,7 +200,7 @@ class Updater: if newversion and self.root: status = self.root.nametowidget(f'.{appname.lower()}.status') # LANG: Update Available Text - status['text'] = _("{NEWVER} is available").format(NEWVER=newversion.title) + status['text'] = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title) self.root.update_idletasks() else: From f3fe146c6654d90e52c7d4af196ea9642b6132f3 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 17:36:37 -0400 Subject: [PATCH 05/33] [1812] Additional Translation Handover --- myNotebook.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/myNotebook.py b/myNotebook.py index 070b28e1..b51ada78 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -12,11 +12,8 @@ from __future__ import annotations import sys import tkinter as tk from tkinter import ttk, messagebox -from typing import TYPE_CHECKING from PIL import ImageGrab - -if TYPE_CHECKING: - def _(x: str) -> str: return x +from l10n import translations as tr if sys.platform == 'win32': PAGEFG = 'SystemWindowText' @@ -108,8 +105,8 @@ class EntryMenu(ttk.Entry): if img: # Hijack existing translation, yes it doesn't exactly match here. # LANG: Generic error prefix - following text is from Frontier auth service; - messagebox.showwarning(_('Error'), - _('Cannot paste non-text content.')) # LANG: Can't Paste Images or Files in Text + messagebox.showwarning(tr.tl('Error'), + tr.tl('Cannot paste non-text content.')) # LANG: Can't Paste Images or Files in Text return text = self.clipboard_get() if self.selection_present() and text: From 96c78dae008a91e0f5f23c3a2a89c42ad031598f Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 17:40:30 -0400 Subject: [PATCH 06/33] [Minor] Flake8 is Grumpy --- myNotebook.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/myNotebook.py b/myNotebook.py index b51ada78..43acfebd 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -104,9 +104,10 @@ class EntryMenu(ttk.Entry): img = ImageGrab.grabclipboard() if img: # Hijack existing translation, yes it doesn't exactly match here. - # LANG: Generic error prefix - following text is from Frontier auth service; - messagebox.showwarning(tr.tl('Error'), - tr.tl('Cannot paste non-text content.')) # LANG: Can't Paste Images or Files in Text + messagebox.showwarning( + tr.tl('Error'), # LANG: Generic error prefix - following text is from Frontier auth service; + tr.tl('Cannot paste non-text content.') # LANG: Can't Paste Images or Files in Text + ) return text = self.clipboard_get() if self.selection_present() and text: From c62592e95e9dddaefee50180a969de3b07be1ca8 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 18:05:03 -0400 Subject: [PATCH 07/33] [1812] Update Docs --- docs/Translations.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/Translations.md b/docs/Translations.md index 959897f2..1de83c3b 100644 --- a/docs/Translations.md +++ b/docs/Translations.md @@ -8,15 +8,20 @@ Translations are handled on [OneSky](https://oneskyapp.com/), specifically in [t ### Setting it up in the code -#### Call `_(...)` +#### Call `tr.tl(...)` If you add any new strings that appear in the application UI, e.g. new configuration options, then you should specify them as: - _('Text that appears in UI') -`_()` is a special global function that then handles the translation, using its single argument, plus the configured language, to look up the appropriate text. + tr.tl('Text that appears in UI') + +In order to do this, you must add the following import: + +`from l10n import translations as tr` + +`tr.tl()` is a function that then handles the translation, using its single argument, plus the configured language, to look up the appropriate text. If you need to specify something in the text that shouldn't be translated then use the form: - _('Some text with a {WORD} not translated').format(WORD='word') + tr.tl('Some text with a {WORD} not translated').format(WORD='word') This way 'word' will always be used literally. #### Add a LANG comment @@ -28,8 +33,9 @@ end of the line in your usage**. If both comments exist, the one on the current line is preferred over the one above ```py +from l10n import translations as tr # LANG: this says stuff. -_('stuff') +tr.tl('stuff') ``` #### Edit `L10n/en.template` to add the phrase @@ -43,7 +49,7 @@ e.g. "Authentication successful" = "Authentication successful"; which matches with: - self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website + self.status['text'] = tr.tl('Authentication successful') # Successfully authenticated with the Frontier website and @@ -51,12 +57,12 @@ and "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name"; which matches with: - nb.Label(plugsframe, text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')).grid( # Help text in settings + nb.Label(plugsframe, text=tr.tl("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')).grid( # Help text in settings `{CR}` is handled in `l10n.py`, translating to a unicode `\n`. See the code in`l10n.py` for any other such special substitutions. You can even use other translations within a given string, e.g.: - _("One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.".format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')) + tr.tl("One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.".format(PLUGINS=tr.tl('Plugins'), FILE=tr.tl('File'), SETTINGS=tr.tl('Settings'), DISABLED='.disabled')) /* Popup body: Warning about plugins without Python 3.x support [EDMarketConnector.py] */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name."; From 10be2c203199261f440a8b99a6b1ae95b4711033 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 18:32:19 -0400 Subject: [PATCH 08/33] [Docs] Update Plugin Docs --- PLUGINS.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 3048f345..03f9d31c 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -1193,19 +1193,17 @@ widget if you need to display routine status information. ## Localisation You can localise your plugin to one of the languages that EDMarketConnector -itself supports. Add the following boilerplate near the top of each source +itself supports. Add the following import near the top of each source file that contains strings that needs translating: ```python -import l10n -import functools -_ = functools.partial(l10n.Translations.translate, context=__file__) +from l10n import translations as tr ``` -Wrap each string that needs translating with the `_()` function, e.g.: +Wrap each string that needs translating with the `tr.tl()` function, e.g.: ```python - somewidget["text"] = _("Happy!") + somewidget["text"] = tr.tl("Happy!") ``` If you display localized strings in EDMarketConnector's main window you should From 91e4e7998bf09bae760c1b0be246f52e0bee3587 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 19:18:01 -0400 Subject: [PATCH 09/33] [1812] Update Docs and Fix Compat Layer --- PLUGINS.md | 16 ++++++++++++---- l10n.py | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 03f9d31c..d050afb0 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -1193,19 +1193,27 @@ widget if you need to display routine status information. ## Localisation You can localise your plugin to one of the languages that EDMarketConnector -itself supports. Add the following import near the top of each source +itself supports. Add the following boilerplate near the top of the source file that contains strings that needs translating: ```python -from l10n import translations as tr +import l10n +import functools +plugin_tl = functools.partial(l10n.translations.tl, context=__file__) + ``` -Wrap each string that needs translating with the `tr.tl()` function, e.g.: +Wrap each string that needs translating with the `plugin_tl()` function, e.g.: ```python - somewidget["text"] = tr.tl("Happy!") + somewidget["text"] = plugin_tl("Happy!") ``` +Note that you can name the "plugin_tl" function whatever you want - just make sure to stay consistent! +Many plugins use `_` as the singleton name. We discourage that in versions 5.11 onward, but it should still work. +If your plugin has multiple files that need translations, simply import the `plugin_tl` function to that location. +You should only need to add the boilerplate once. + If you display localized strings in EDMarketConnector's main window you should refresh them in your `prefs_changed` function in case the user has changed their preferred language. diff --git a/l10n.py b/l10n.py index f3215062..e72499de 100755 --- a/l10n.py +++ b/l10n.py @@ -86,7 +86,7 @@ class Translations: """ self.translations = {None: {}} # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. - # Migrate to calling Translations.translate directly. + # Migrate to calling translations.translate or tr.tl directly. builtins.__dict__['_'] = lambda x: str(x).replace(r'\"', '"').replace('{CR}', '\n') def install(self, lang: str | None = None) -> None: # noqa: CCR001 @@ -131,7 +131,7 @@ class Translations: logger.exception(f'Exception occurred while parsing {lang}.strings in plugin {plugin}') # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. - # Migrate to calling Translations.translate directly. + # Migrate to calling translations.translate or tr.tl directly. builtins.__dict__['_'] = self.translate def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]: @@ -351,7 +351,7 @@ class _Translations(Translations): super().__init__() -Translations: Translations = _Translations() # type: ignore # Yes, I know this is awful. But we need it for compat. +Translations = translations # Yes, I know this is awful renaming garbage. But we need it for compat. # End Deprecation Zone # generate template strings file - like xgettext From 26c8a8be6e168aa9c6a44dc2ec40683cedaa52c5 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 22 Apr 2024 19:22:34 -0400 Subject: [PATCH 10/33] [Minor] [Incoherent Type Hinting Noises] --- l10n.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/l10n.py b/l10n.py index e72499de..c4a92a67 100755 --- a/l10n.py +++ b/l10n.py @@ -351,7 +351,8 @@ class _Translations(Translations): super().__init__() -Translations = translations # Yes, I know this is awful renaming garbage. But we need it for compat. +# Yes, I know this is awful renaming garbage. But we need it for compat. +Translations: Translations = translations # type: ignore # End Deprecation Zone # generate template strings file - like xgettext From 4a1a107e032a85074d232484acb43ad509ef0b10 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Wed, 24 Apr 2024 19:58:39 -0400 Subject: [PATCH 11/33] [1812] Adapt Localized String Search --- scripts/find_localised_strings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index ef861bb7..5911d64c 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -38,8 +38,9 @@ def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: out = [] for n in ast.iter_child_nodes(statement): out.extend(find_calls_in_stmt(n)) - if isinstance(statement, ast.Call) and get_func_name(statement.func) == '_': - out.append(statement) + if isinstance(statement, ast.Call) and get_func_name(statement.func) in ('tr', 'translations'): + if ast.unparse(statement).find('.tl') != -1 or ast.unparse(statement).find('translate') != -1: + out.append(statement) return out From e573b8996647ccda1555e6bd6af5c694c333b630 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Sat, 27 Apr 2024 14:58:03 -0400 Subject: [PATCH 12/33] [1654] Updated setCommanderShip entry --- plugins/inara.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/plugins/inara.py b/plugins/inara.py index 810f7523..47c857e6 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -553,23 +553,6 @@ def journal_entry( # noqa: C901, CCR001 # Ship change if event_name == 'Loadout' and this.shipswap: - cur_ship = { - 'shipType': state['ShipType'], - 'shipGameID': state['ShipID'], - 'shipName': state['ShipName'], # Can be None - 'shipIdent': state['ShipIdent'], # Can be None - 'isCurrentShip': True, - } - - if state['HullValue']: - cur_ship['shipHullValue'] = state['HullValue'] - - if state['ModulesValue']: - cur_ship['shipModulesValue'] = state['ModulesValue'] - - cur_ship['shipRebuyCost'] = state['Rebuy'] - new_add_event('setCommanderShip', entry['timestamp'], cur_ship) - this.loadout = make_loadout(state) new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) this.shipswap = False @@ -857,7 +840,7 @@ def journal_entry( # noqa: C901, CCR001 for ship in this.fleet: new_add_event('setCommanderShip', entry['timestamp'], ship) # Loadout - if event_name == 'Loadout' and not this.newsession: + if event_name == 'Loadout': loadout = make_loadout(state) if this.loadout != loadout: this.loadout = loadout @@ -871,6 +854,26 @@ def journal_entry( # noqa: C901, CCR001 new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) + cur_ship = { + 'shipType': state['ShipType'], + 'shipGameID': state['ShipID'], + 'shipName': state['ShipName'], # Can be None + 'shipIdent': state['ShipIdent'], # Can be None + 'isCurrentShip': True, + 'shipMaxJumpRange': entry['MaxJumpRange'], + 'shipCargoCapacity': entry['CargoCapacity'] + } + if state['HullValue']: + cur_ship['shipHullValue'] = state['HullValue'] + + if state['ModulesValue']: + cur_ship['shipModulesValue'] = state['ModulesValue'] + + if state['Rebuy']: + cur_ship['shipRebuyCost'] = state['Rebuy'] + + new_add_event('setCommanderShip', entry['timestamp'], cur_ship) + # Stored modules if event_name == 'StoredModules': items = {mod['StorageSlot']: mod for mod in entry['Items']} # Impose an order From 6dbdfe50b9f0249b86f0b6593742a6265e7f4a77 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Sun, 28 Apr 2024 16:28:28 -0400 Subject: [PATCH 13/33] [Docs] Add CodeQL Workflow and Security Guide --- .github/SECURITY.md | 13 +++++ .github/pull_request_template.md | 18 ++++++ .github/workflows/codeql.yml | 96 ++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 .github/SECURITY.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/codeql.yml diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..b8a0b244 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,13 @@ +# Reporting Security Issues + +EDMC takes security very seriously. Our users trust us to provide a secure and safe tool to support their experience in Elite. + +In general, the best way to report a major security issue with us that should not be publically discussed is to email our maintainer teams. + +The best point of contact for this is rixxan@hullseals.space. When contacting, be sure to include as much information in your report. + +As soon as your report is processed, we'll get in touch to make sure we quickly move ahead with fixing the issue and will lay out a timeline for public disclosure and fixes. + +Another method of reporting vulnerabilities is to open a new Bug Report [here](https://github.com/EDCD/EDMarketConnector/issues/new?assignees=&labels=bug%2C+unconfirmed&projects=&template=bug_report.md&title=). + +If reporting a security issue here, do not include details as to the issue or steps to reproduce, simply indicate you have found a potential security bug and would like us to contact you directly. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..0d1c9ac1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + +# Description + + +# Example Images + + +# Type of Change + + +# How Tested + + +# Notes + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..35a792fa --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,96 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches-ignore: + - 'main' + - 'stable' + - 'releases' + - 'beta' + pull_request: + branches: [ develop ] + schedule: + - cron: '38 5 * * 4' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: 'ubuntu-latest' + timeout-minutes: 360 + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From fc2426d8b989df6ca21507c03063ca1790ada266 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 17:29:32 +0000 Subject: [PATCH 14/33] build(deps-dev): bump safety from 3.0.1 to 3.2.0 Bumps [safety](https://github.com/pyupio/safety) from 3.0.1 to 3.2.0. - [Release notes](https://github.com/pyupio/safety/releases) - [Changelog](https://github.com/pyupio/safety/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyupio/safety/compare/3.0.1...3.2.0) --- updated-dependencies: - dependency-name: safety dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 41c3a928..42877907 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ flake8-use-fstring==1.4 mypy==1.9.0 pep8-naming==0.13.3 -safety==3.0.1 +safety==3.2.0 types-requests==2.31.0.20240311 types-pkg-resources==0.1.3 From 45cd577bec11d78caf309d750dfb8cca922a3417 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 17:29:49 +0000 Subject: [PATCH 15/33] build(deps-dev): bump pytest-cov from 4.1.0 to 5.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 41c3a928..ef2728ca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -39,7 +39,7 @@ py2exe==0.13.0.1; sys_platform == 'win32' # Testing pytest==8.1.1 -pytest-cov==4.1.0 # Pytest code coverage support +pytest-cov==5.0.0 # Pytest code coverage support coverage[toml]==7.4.4 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 # For manipulating folder permissions and the like. From 9ce7206cae156247221d048e6e5c5f812ecc3929 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 17:29:51 +0000 Subject: [PATCH 16/33] build(deps-dev): bump flake8-json from 23.7.0 to 24.4.0 Bumps [flake8-json](https://github.com/pycqa/flake8-json) from 23.7.0 to 24.4.0. - [Commits](https://github.com/pycqa/flake8-json/compare/23.7.0...24.4.0) --- updated-dependencies: - dependency-name: flake8-json dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 41c3a928..fdab1681 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ flake8-annotations-coverage==0.0.6 flake8-cognitive-complexity==0.1.0 flake8-comprehensions==3.14.0 flake8-docstrings==1.7.0 -flake8-json==23.7.0 +flake8-json==24.4.0 flake8-noqa==1.4.0 flake8-polyfill==1.0.2 flake8-use-fstring==1.4 From 53dd3e3ee5bd35e984007a6f063ecd437b2902f0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 1 May 2024 17:31:06 +0000 Subject: [PATCH 17/33] updating submodules --- coriolis-data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coriolis-data b/coriolis-data index 05b16a4c..8adfd86b 160000 --- a/coriolis-data +++ b/coriolis-data @@ -1 +1 @@ -Subproject commit 05b16a4c716980ea95a46d29205f7d3b1f957fb4 +Subproject commit 8adfd86b64e8c14e873d2f5123d88ca6743420b9 From e60d0c8813192b3c0d5ec788c58c4eeb41329993 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 00:04:43 +0000 Subject: [PATCH 18/33] build(deps-dev): bump coverage[toml] from 7.4.4 to 7.5.0 Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 7.4.4 to 7.5.0. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.4.4...7.5.0) --- updated-dependencies: - dependency-name: coverage[toml] dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ef2728ca..8446d00a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -40,7 +40,7 @@ py2exe==0.13.0.1; sys_platform == 'win32' # Testing pytest==8.1.1 pytest-cov==5.0.0 # Pytest code coverage support -coverage[toml]==7.4.4 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs +coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 # For manipulating folder permissions and the like. pywin32==306; sys_platform == 'win32' From 5cdf4b9ce3ff4bfa3e168bb79786b5fd728588e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 00:07:11 +0000 Subject: [PATCH 19/33] build(deps-dev): bump pytest from 8.1.1 to 8.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8446d00a..247f589e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -38,7 +38,7 @@ grip==4.6.2 py2exe==0.13.0.1; sys_platform == 'win32' # Testing -pytest==8.1.1 +pytest==8.2.0 pytest-cov==5.0.0 # Pytest code coverage support coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 From 88bfd8ca8bb9b4f05efa6daf65120397ba7fef59 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Thu, 2 May 2024 15:02:43 -0400 Subject: [PATCH 20/33] [1268] Handover to Path.is_reserved() --- util_ships.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/util_ships.py b/util_ships.py index 8bbfd813..b2105106 100644 --- a/util_ships.py +++ b/util_ships.py @@ -5,18 +5,21 @@ Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. """ +from pathlib import Path from edmc_data import ship_name_map def ship_file_name(ship_name: str, ship_type: str) -> str: """Return a ship name suitable for a filename.""" name = str(ship_name or ship_name_map.get(ship_type.lower(), ship_type)).strip() - if name.endswith('.'): - name = name[:-2] - if name.lower() in ('con', 'prn', 'aux', 'nul', - 'com0', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', - 'lpt0', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'): - name += '_' + # Handle suffix using Pathlib's with_suffix method + name = Path(name).with_suffix("").name - return name.translate({ord(x): '_' for x in ('\0', '<', '>', ':', '"', '/', '\\', '|', '?', '*')}) + # Check if the name is a reserved filename + if Path(name).is_reserved(): + name += "_" + + return name.translate( + {ord(x): "_" for x in ("\0", "<", ">", ":", '"', "/", "\\", "|", "?", "*")} + ) From 2469ca2132e374441fee435a9a9d233b91ca956d Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Thu, 2 May 2024 20:11:07 -0400 Subject: [PATCH 21/33] [Minor] Correct Email Address --- .github/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index b8a0b244..a926715b 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,7 +4,7 @@ EDMC takes security very seriously. Our users trust us to provide a secure and s In general, the best way to report a major security issue with us that should not be publically discussed is to email our maintainer teams. -The best point of contact for this is rixxan@hullseals.space. When contacting, be sure to include as much information in your report. +The best point of contact for this is edmc@hullseals.space. When contacting, be sure to include as much information in your report. As soon as your report is processed, we'll get in touch to make sure we quickly move ahead with fixing the issue and will lay out a timeline for public disclosure and fixes. From 83fdaab61b7b472352c38d42a17af83c6e25297b Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Thu, 2 May 2024 20:28:16 -0400 Subject: [PATCH 22/33] Update codeql.yml --- .github/workflows/codeql.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 35a792fa..a709c7dc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,6 @@ name: "CodeQL" on: push: branches-ignore: - - 'main' - 'stable' - 'releases' - 'beta' From 8198d779c3e7479bfc75dfae6faa47a25dc9a3ca Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Thu, 2 May 2024 21:34:58 -0400 Subject: [PATCH 23/33] [830] Open Log Folder Natively --- EDMarketConnector.py | 6 +++--- prefs.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 3ab97eb5..1ce34628 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -18,6 +18,7 @@ import subprocess import sys import threading import webbrowser +import tempfile from os import chdir, environ, path from time import localtime, strftime, time from typing import TYPE_CHECKING, Any, Literal @@ -47,8 +48,6 @@ if __name__ == '__main__': # output until after this redirect is done, if needed. if getattr(sys, 'frozen', False): # By default py2exe tries to write log to dirname(sys.executable) which fails when installed - import tempfile - # unbuffered not allowed for text in python3, so use `1 for line buffering log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log') sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here. @@ -651,7 +650,8 @@ class AppWindow: self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates... # About E:D Market Connector self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w)) - self.help_menu.add_command(command=prefs.help_open_log_folder) # Open Log Folder + logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname + self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder self.menubar.add_cascade(menu=self.help_menu) if sys.platform == 'win32': diff --git a/prefs.py b/prefs.py index 285ef0d7..81885257 100644 --- a/prefs.py +++ b/prefs.py @@ -8,14 +8,12 @@ import pathlib import sys import tempfile import tkinter as tk -import webbrowser from os import system from os.path import expanduser, expandvars, join, normpath from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812 from tkinter import ttk from types import TracebackType from typing import TYPE_CHECKING, Any, Callable, Optional, Type - import myNotebook as nb # noqa: N813 import plug from config import appversion_nobuild, config @@ -44,14 +42,21 @@ if TYPE_CHECKING: def help_open_log_folder() -> None: """Open the folder logs are stored in.""" - logfile_loc = pathlib.Path(tempfile.gettempdir()) - logfile_loc /= f'{appname}' + logger.warning( + DeprecationWarning("This function is deprecated, use open_log_folder instead. " + "This function will be removed in 6.0 or later") + ) + open_folder(pathlib.Path(tempfile.gettempdir()) / appname) + + +def open_folder(file: pathlib.Path) -> None: + """Open the given file in the OS file explorer.""" if sys.platform.startswith('win'): # On Windows, use the "start" command to open the folder - system(f'start "" "{logfile_loc}"') + system(f'start "" "{file}"') elif sys.platform.startswith('linux'): # On Linux, use the "xdg-open" command to open the folder - system(f'xdg-open "{logfile_loc}"') + system(f'xdg-open "{file}"') class PrefsVersion: @@ -300,6 +305,9 @@ class PreferencesDialog(tk.Toplevel): ): self.geometry(f"+{position.left}+{position.top}") + # Set Log Directory + self.logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname + def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: output_frame = nb.Frame(root_notebook) output_frame.columnconfigure(0, weight=1) @@ -625,7 +633,7 @@ class PreferencesDialog(tk.Toplevel): config_frame, # LANG: Label on button used to open a filesystem folder text=_('Open Log Folder'), # Button that opens a folder in Explorer/Finder - command=lambda: help_open_log_folder() + command=lambda: open_folder(self.logfile_loc) ).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row) # Big spacer @@ -884,7 +892,7 @@ class PreferencesDialog(tk.Toplevel): plugins_frame, # LANG: Label on button used to open a filesystem folder text=_('Open'), # Button that opens a folder in Explorer/Finder - command=lambda: webbrowser.open(f'file:///{config.plugin_dir_path}') + command=lambda: open_folder(config.plugin_dir_path) ).grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row) enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS)) From ebc62ae8e0c8653d9fd75df70e2e0205b167401f Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 10:17:06 -0400 Subject: [PATCH 24/33] [Minor] Update modules.json --- modules.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules.json b/modules.json index 27e54626..16e72f7f 100644 --- a/modules.json +++ b/modules.json @@ -326,6 +326,9 @@ "hpt_antiunknownshutdown_tiny": { "mass": 1.3 }, + "hpt_antiunknownshutdown_tiny_v2": { + "mass": 3 + }, "hpt_atdumbfiremissile_fixed_large": { "mass": 8 }, @@ -446,6 +449,9 @@ "hpt_causticmissile_fixed_medium": { "mass": 4 }, + "hpt_causticsinklauncher_turret_tiny": { + "mass": 1.7 + }, "hpt_chafflauncher_tiny": { "mass": 1.3 }, @@ -833,6 +839,9 @@ "hpt_slugshot_turret_small": { "mass": 2 }, + "hpt_xenoscanner_advanced_tiny": { + "mass": 3 + }, "hpt_xenoscanner_basic_tiny": { "mass": 1.3 }, @@ -1352,6 +1361,12 @@ "int_engine_size8_class5": { "mass": 160 }, + "int_expmodulestabiliser_size3_class3": { + "mass": 8 + }, + "int_expmodulestabiliser_size5_class3": { + "mass": 20 + }, "int_fighterbay_size5_class1": { "mass": 20 }, From 891c56d515053032745a08a9fe407d0e2fcba218 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 13:40:49 -0400 Subject: [PATCH 25/33] [Submodules] Update Submodules --- FDevIDs | 2 +- coriolis-data | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FDevIDs b/FDevIDs index 7cffab3d..9b3f4061 160000 --- a/FDevIDs +++ b/FDevIDs @@ -1 +1 @@ -Subproject commit 7cffab3d913b788f981923687203399c22cf358f +Subproject commit 9b3f40612017b43a8b826017e1e2befebd9074f2 diff --git a/coriolis-data b/coriolis-data index 05b16a4c..8adfd86b 160000 --- a/coriolis-data +++ b/coriolis-data @@ -1 +1 @@ -Subproject commit 05b16a4c716980ea95a46d29205f7d3b1f957fb4 +Subproject commit 8adfd86b64e8c14e873d2f5123d88ca6743420b9 From 1bb2062c6f3b6459900d3e8effad47abc58969eb Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 16:06:38 -0400 Subject: [PATCH 26/33] [Minor] Update modules.json --- modules.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules.json b/modules.json index 27e54626..16e72f7f 100644 --- a/modules.json +++ b/modules.json @@ -326,6 +326,9 @@ "hpt_antiunknownshutdown_tiny": { "mass": 1.3 }, + "hpt_antiunknownshutdown_tiny_v2": { + "mass": 3 + }, "hpt_atdumbfiremissile_fixed_large": { "mass": 8 }, @@ -446,6 +449,9 @@ "hpt_causticmissile_fixed_medium": { "mass": 4 }, + "hpt_causticsinklauncher_turret_tiny": { + "mass": 1.7 + }, "hpt_chafflauncher_tiny": { "mass": 1.3 }, @@ -833,6 +839,9 @@ "hpt_slugshot_turret_small": { "mass": 2 }, + "hpt_xenoscanner_advanced_tiny": { + "mass": 3 + }, "hpt_xenoscanner_basic_tiny": { "mass": 1.3 }, @@ -1352,6 +1361,12 @@ "int_engine_size8_class5": { "mass": 160 }, + "int_expmodulestabiliser_size3_class3": { + "mass": 8 + }, + "int_expmodulestabiliser_size5_class3": { + "mass": 20 + }, "int_fighterbay_size5_class1": { "mass": 20 }, From 690523c9169a88c90fd5b7073c898e216cd8551d Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 16:11:30 -0400 Subject: [PATCH 27/33] [2228] Handle Unknown FSD Ranges --- edmc_data.py | 3 ++- edshipyard.py | 22 +++++++++++++--------- outfitting.py | 4 ++-- ships.json | 3 +++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/edmc_data.py b/edmc_data.py index d76badc6..e8c9518b 100644 --- a/edmc_data.py +++ b/edmc_data.py @@ -357,7 +357,7 @@ outfitting_standard_map = { 'guardianpowerdistributor': 'Guardian Hybrid Power Distributor', 'guardianpowerplant': 'Guardian Hybrid Power Plant', 'hyperdrive': 'Frame Shift Drive', - ('hyperdrive', 'overcharge'): 'Frame Shift Drive', + ('hyperdrive', 'overcharge'): 'Frame Shift Drive (SCO)', 'lifesupport': 'Life Support', # 'planetapproachsuite': handled separately 'powerdistributor': 'Power Distributor', @@ -501,6 +501,7 @@ ship_name_map = { 'mamba': 'Mamba', 'orca': 'Orca', 'python': 'Python', + 'python_nx': 'Python Mk II', 'scout': 'Taipan Fighter', 'sidewinder': 'Sidewinder', 'testbuggy': 'Scarab', diff --git a/edshipyard.py b/edshipyard.py index 1660ad7e..15936dfc 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -106,7 +106,7 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 else: name = module['name'] # type: ignore - if name == 'Frame Shift Drive': + if name == 'Frame Shift Drive' or name == 'Frame Shift Drive (SCO)': fsd = module # save for range calculation if mods.get('OutfittingFieldType_FSDOptimalMass'): @@ -167,15 +167,19 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 try: mass += ships[ship_name_map[data['ship']['name'].lower()]]['hullMass'] string += f'Mass : {mass:.2f} T empty\n {mass + fuel + cargo:.2f} T full\n' + maxfuel = fsd.get('maxfuel', 0) # type: ignore + fuelmul = fsd.get('fuelmul', 0) # type: ignore - multiplier = pow(min(fuel, fsd['maxfuel']) / fsd['fuelmul'], 1.0 # type: ignore - / fsd['fuelpower']) * fsd['optmass'] # type: ignore - - range_unladen = multiplier / (mass + fuel) + jumpboost - range_laden = multiplier / (mass + fuel + cargo) + jumpboost - # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with - # a locale that uses comma for decimal separator. - string += f'Range : {range_unladen:.2f} LY unladen\n {range_laden:.2f} LY laden\n' + try: + multiplier = pow(min(fuel, maxfuel) / fuelmul, 1.0 / fsd['fuelpower']) * fsd['optmass'] # type: ignore + range_unladen = multiplier / (mass + fuel) + jumpboost + range_laden = multiplier / (mass + fuel + cargo) + jumpboost + # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with + # a locale that uses comma for decimal separator. + except ZeroDivisionError: + range_unladen = range_laden = 0.0 + string += (f'Range : {range_unladen:.2f} LY current without cargo\n' + f' {range_laden:.2f} LY current with cargo\n') except Exception: if __debug__: diff --git a/outfitting.py b/outfitting.py index 5632687c..770c47cd 100644 --- a/outfitting.py +++ b/outfitting.py @@ -222,7 +222,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 (new['class'], new['rating']) = (str(name[2][4:]), 'H') elif len(name) > 4 and name[1] == 'hyperdrive': # e.g. Int_Hyperdrive_Overcharge_Size6_Class3 - (new['class'], new['rating']) = (str(name[4][-1:]), 'C') + (new['class'], new['rating']) = (str(name[3][-1:]), rating_map[name[4][-1:]]) else: if len(name) < 3: @@ -257,7 +257,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 if not m: print(f'No data for module {key}') - elif new['name'] == 'Frame Shift Drive': + elif new['name'] == 'Frame Shift Drive' or new['name'] == 'Frame Shift Drive (SCO)': assert 'mass' in m and 'optmass' in m and 'maxfuel' in m and 'fuelmul' in m and 'fuelpower' in m, m else: diff --git a/ships.json b/ships.json index 4a40ba4f..65a2989e 100644 --- a/ships.json +++ b/ships.json @@ -89,6 +89,9 @@ "Python": { "hullMass": 350 }, + "Python Mk II": { + "hullMass": 450 + }, "Sidewinder": { "hullMass": 25 }, From fcbb89b0c9b207f54361c5dd6b2b0fb592c59adb Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 17:01:24 -0400 Subject: [PATCH 28/33] Merge pull request #2230 from HullSeals/fix/2228/tce-unknown-modules [2228] Handle Unknown FSD Ranges --- edmc_data.py | 3 ++- edshipyard.py | 22 +++++++++++++--------- outfitting.py | 4 ++-- ships.json | 3 +++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/edmc_data.py b/edmc_data.py index b7ed4556..6c2e809a 100644 --- a/edmc_data.py +++ b/edmc_data.py @@ -357,7 +357,7 @@ outfitting_standard_map = { 'guardianpowerdistributor': 'Guardian Hybrid Power Distributor', 'guardianpowerplant': 'Guardian Hybrid Power Plant', 'hyperdrive': 'Frame Shift Drive', - ('hyperdrive', 'overcharge'): 'Frame Shift Drive', + ('hyperdrive', 'overcharge'): 'Frame Shift Drive (SCO)', 'lifesupport': 'Life Support', # 'planetapproachsuite': handled separately 'powerdistributor': 'Power Distributor', @@ -501,6 +501,7 @@ ship_name_map = { 'mamba': 'Mamba', 'orca': 'Orca', 'python': 'Python', + 'python_nx': 'Python Mk II', 'scout': 'Taipan Fighter', 'sidewinder': 'Sidewinder', 'testbuggy': 'Scarab', diff --git a/edshipyard.py b/edshipyard.py index 1660ad7e..15936dfc 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -106,7 +106,7 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 else: name = module['name'] # type: ignore - if name == 'Frame Shift Drive': + if name == 'Frame Shift Drive' or name == 'Frame Shift Drive (SCO)': fsd = module # save for range calculation if mods.get('OutfittingFieldType_FSDOptimalMass'): @@ -167,15 +167,19 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 try: mass += ships[ship_name_map[data['ship']['name'].lower()]]['hullMass'] string += f'Mass : {mass:.2f} T empty\n {mass + fuel + cargo:.2f} T full\n' + maxfuel = fsd.get('maxfuel', 0) # type: ignore + fuelmul = fsd.get('fuelmul', 0) # type: ignore - multiplier = pow(min(fuel, fsd['maxfuel']) / fsd['fuelmul'], 1.0 # type: ignore - / fsd['fuelpower']) * fsd['optmass'] # type: ignore - - range_unladen = multiplier / (mass + fuel) + jumpboost - range_laden = multiplier / (mass + fuel + cargo) + jumpboost - # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with - # a locale that uses comma for decimal separator. - string += f'Range : {range_unladen:.2f} LY unladen\n {range_laden:.2f} LY laden\n' + try: + multiplier = pow(min(fuel, maxfuel) / fuelmul, 1.0 / fsd['fuelpower']) * fsd['optmass'] # type: ignore + range_unladen = multiplier / (mass + fuel) + jumpboost + range_laden = multiplier / (mass + fuel + cargo) + jumpboost + # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with + # a locale that uses comma for decimal separator. + except ZeroDivisionError: + range_unladen = range_laden = 0.0 + string += (f'Range : {range_unladen:.2f} LY current without cargo\n' + f' {range_laden:.2f} LY current with cargo\n') except Exception: if __debug__: diff --git a/outfitting.py b/outfitting.py index d290ad3b..6314d3da 100644 --- a/outfitting.py +++ b/outfitting.py @@ -224,7 +224,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 (new['class'], new['rating']) = (str(name[2][4:]), 'H') elif len(name) > 4 and name[1] == 'hyperdrive': # e.g. Int_Hyperdrive_Overcharge_Size6_Class3 - (new['class'], new['rating']) = (str(name[4][-1:]), 'C') + (new['class'], new['rating']) = (str(name[3][-1:]), rating_map[name[4][-1:]]) else: if len(name) < 3: @@ -259,7 +259,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 if not m: print(f'No data for module {key}') - elif new['name'] == 'Frame Shift Drive': + elif new['name'] == 'Frame Shift Drive' or new['name'] == 'Frame Shift Drive (SCO)': assert 'mass' in m and 'optmass' in m and 'maxfuel' in m and 'fuelmul' in m and 'fuelpower' in m, m else: diff --git a/ships.json b/ships.json index 4a40ba4f..65a2989e 100644 --- a/ships.json +++ b/ships.json @@ -89,6 +89,9 @@ "Python": { "hullMass": 350 }, + "Python Mk II": { + "hullMass": 450 + }, "Sidewinder": { "hullMass": 25 }, From ae748e8d266e7dc027fb2d490b35a4269a09f456 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 7 May 2024 17:41:33 -0400 Subject: [PATCH 29/33] [RELEASE] 5.10.5 --- ChangeLog.md | 18 ++++++++++++++++++ L10n/ja.strings | 14 ++++++++++++++ L10n/pl.strings | 29 +++++++++++++++++++++++++++++ L10n/pt-BR.strings | 29 +++++++++++++++++++++++++++++ config/__init__.py | 2 +- 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 949b6bf8..324d1f8d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,24 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in the source (not distributed with the Windows installer) for the currently used version. --- +Release 5.10.5 +=== +This release contains a fix for a bug that could crash EDMC's console versions when reading outfitting information +from the new SCO Frame Shift Drive modules. + +Please note that this does not offer full support for the new SCO modules or the Python Mk II. More support will +be added in a future update. + +**Changes and Enhancements** +* Updated Translations +* Added limited data regarding the Python Mk II +* Added a few Coriolis module information entries + +**Bug Fixes** +* Fixed a bug that could cause the new SCO modules to display improper ratings or sizes +* Fixed a bug where the new SCO modules would display as a normal Frame Shift Drive +* Fixed a bug which could crash EDMC if the exact details of a Frame Shift Drive were unknown + Release 5.10.4 === This release contains updated dependencies, modules files, translations, and adds two new EDDN schemas. It also diff --git a/L10n/ja.strings b/L10n/ja.strings index 82d9f8df..75acb252 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -231,6 +231,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "有効にしている1つ以上のプラグインがロードに失敗しました。'{FILE}' > '{SETTINGS}' メニューで表示される設定ダイアログの'{PLUGINS}' タブの一覧を確認してください。この問題は誤ったフォルダ構造によって引き起こされます。load.pyファイルはplugins/プラグイン名/plug-in.pyとして配置される必要があります。\n\nプラグインを無効にするにはフォルダ名の最後に'{DISABLED}'を追加してください。"; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "一つ以上のURLプロバイダが無効であったためリセットされました:\n\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} は {OLDPROV} に設定され、 {NEWPROV} にリセットされました\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: デフォルトプロバイダーのリセット"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "完全なCMDRログインを待機しています"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "ジャーナルディレクトリは既にロックされています"; @@ -789,3 +801,5 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "所有船"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} があります"; diff --git a/L10n/pl.strings b/L10n/pl.strings index 547406b1..efde05be 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -213,12 +213,36 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Co najmniej jeden z uruchomionych pluginów nie wspiera Python 3.x. Sprawdź listę w '{FILE}' > '{SETTINGS}', sekcja '{PLUGINS}'. Upewnij się, że dostępna jest aktualna wersja pluginu, w przeciwnym razie poinformuj twórcę, że należy zaktualizować kod do wersji Python 3.x.\n\nMożesz wyłączyć plugin, dodając '{DISABLED}' na koniec nazwy jego folderu."; +/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ +"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "Pliki FDevID nie znalezione. Część funkcjonalności związanej z materiałami może nie działać. \n\nChcesz otworzyć stronę Wiki dotyczacą konfiguracji podmodułów?"; + +/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ +"FDevIDs: Missing Commodity Files" = "FDevIDs: Brakuje plików z towarami."; + /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Pluginy"; /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Pluginy Nie Wspierające Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Uszkodzone pluginy"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jeden lub więcej plugin nie został załadowany. Sprawdź listę na zakładce '{PLUGINS}' w menu '{FILE}' > '{SETTINGS}'. Może to być spowodowane błedną strukturą katalogów. Plik load.py powinien być umiezczony w plugins/NAZWA PLUGINA/load.py.\n\nMożesz wyłączyć plugin zmieniając nazwę jego folderu tak, aby zawierała '{DISABLED}' na końcu nazwy."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Jeden lub więcej adres URL jest błędny i został zresetowany:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} ustawiony na {OLDPROV} i został zresetowany do {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Dostawca domyślny zresetowany"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Oczekiwanie na pełne zalogowanie do gry"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Katalog dziennika zablokowany"; @@ -471,6 +495,9 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "Informacja o migracji pluginów"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Niedziałające pluginy"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Pluginy wyłączone"; @@ -774,3 +801,5 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Statki"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "Dostępna nowa wersja {NEWVER}"; diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings index 605e0559..b4d9e933 100644 --- a/L10n/pt-BR.strings +++ b/L10n/pt-BR.strings @@ -213,12 +213,36 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Um ou mais dos plugins habilitados não possuem suporte ao Python 3.x. Você pode ver a lista na guia '{PLUGINS}' em '{FILE}' > '{SETTINGS}'. Você deve verificar se existe versão atualizada ou então avisar o desenvolvedor de que ele precisa atualizar o código para o Python 3.x.\n\nVocê pode desabilitar um plugin renomeando sua pasta para que seu nome termine com '{DISABLED}'."; +/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ +"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "Arquivo FDevID não encontrados! Algumas funções de mercadorias podem estar indisponíveis.\n\nGostaria de abrir a Wiki com instruções para configurar sub-módulos?"; + +/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ +"FDevIDs: Missing Commodity Files" = "FDevIDs: Arquivos de Mercadorias não encontrados"; + /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Plugins"; /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins sem Suporte ao Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Plugins Quebrados"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Um ou mais plugins ativados falhou ao carregar. Verifique a lista na aba '{PLUGINS}', em '{FILE}' > '{SETTINGS}'. Isto pode ter ocorrido por um erro na estrutura de pastas. O arquivo load.py deve estar localizado em plugins/NOME_PLUGIN/load.py.\n\nVocê pode desativar plugins renomeando a pasta para ter '.{DISABLED}' ao final do nome."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Um ou mais dos seus Provedores de URL eram inválidos e portanto foram reconfigurados:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} estava como {OLDPROV}, e foi reconfigurado para {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Provedores Padrão Reconfigurados"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Aguardando CMDT entrar no jogo"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Diretório de Jornais já está bloqueado"; @@ -471,6 +495,9 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "Informações de migração de plugins"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Plugins Quebrados"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Plugins desabilitados"; @@ -774,3 +801,5 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Naves"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} está disponível"; diff --git a/config/__init__.py b/config/__init__.py index 1dfe26c3..92347c0b 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -54,7 +54,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.10.4' +_static_appversion = '5.10.5' _cached_version: semantic_version.Version | None = None copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD' From e5f99c29548356993a62ec743da8d61f1b2cad4b Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Wed, 8 May 2024 21:56:52 -0400 Subject: [PATCH 30/33] [#2228] Add SCO Module Information --- edshipyard.py | 4 +- modules.json | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/edshipyard.py b/edshipyard.py index 15936dfc..2c29959f 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -178,8 +178,8 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 # a locale that uses comma for decimal separator. except ZeroDivisionError: range_unladen = range_laden = 0.0 - string += (f'Range : {range_unladen:.2f} LY current without cargo\n' - f' {range_laden:.2f} LY current with cargo\n') + string += (f'Range : {range_unladen:.2f} LY unladen\n' + f' {range_laden:.2f} LY laden\n') except Exception: if __debug__: diff --git a/modules.json b/modules.json index 16e72f7f..b19441d4 100644 --- a/modules.json +++ b/modules.json @@ -1765,6 +1765,216 @@ "int_hullreinforcement_size5_class2": { "mass": 16 }, + "int_hyperdrive_overcharge_size2_class1": { + "mass": 2.5, + "optmass": 60, + "maxfuel": 0.6, + "fuelmul": 0.008, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class2": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class3": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class4": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class5": { + "mass": 2.5, + "optmass": 100, + "maxfuel": 1, + "fuelmul": 0.013, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size3_class1": { + "mass": 5, + "optmass": 100, + "maxfuel": 1.2, + "fuelmul": 0.008, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class2": { + "mass": 2, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class3": { + "mass": 5, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class4": { + "mass": 5, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class5": { + "mass": 5, + "optmass": 167, + "maxfuel": 1.9, + "fuelmul": 0.013, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size4_class1": { + "mass": 10, + "optmass": 350, + "maxfuel": 2, + "fuelmul": 0.008, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class2": { + "mass": 4, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class3": { + "mass": 10, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class4": { + "mass": 10, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class5": { + "mass": 10, + "optmass": 585, + "maxfuel": 3.2, + "fuelmul": 0.013, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size5_class1": { + "mass": 20, + "optmass": 700, + "maxfuel": 3.3, + "fuelmul": 0.008, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class2": { + "mass": 8, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class3": { + "mass": 20, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class4": { + "mass": 20, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class5": { + "mass": 20, + "optmass": 1175, + "maxfuel": 5.2, + "fuelmul": 0.013, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size6_class1": { + "mass": 40, + "optmass": 1200, + "maxfuel": 5.3, + "fuelmul": 0.008, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class2": { + "mass": 16, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class3": { + "mass": 40, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class4": { + "mass": 40, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class5": { + "mass": 40, + "optmass": 2000, + "maxfuel": 8.3, + "fuelmul": 0.013, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size7_class1": { + "mass": 80, + "optmass": 1800, + "maxfuel": 8.5, + "fuelmul": 0.008, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class2": { + "mass": 32, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class3": { + "mass": 80, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class4": { + "mass": 80, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class5": { + "mass": 80, + "optmass": 3000, + "maxfuel": 13.1, + "fuelmul": 0.013, + "fuelpower": 2.75 + }, "int_hyperdrive_size2_class1": { "mass": 2.5, "optmass": 48, From 6ffcd91de5fd95c496b273ded83061a42b82eda6 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Fri, 10 May 2024 17:49:13 -0400 Subject: [PATCH 31/33] [2228] Add PyMkII Armor Details From upcoming Coriolis data --- modules.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules.json b/modules.json index b19441d4..e9e444d6 100644 --- a/modules.json +++ b/modules.json @@ -3363,6 +3363,21 @@ "python_armour_reactive": { "mass": 53 }, + "python_nx_armour_grade1": { + "mass": 0 + }, + "python_nx_armour_grade2": { + "mass": 26 + }, + "python_nx_armour_grade3": { + "mass": 53 + }, + "python_nx_armour_mirrored": { + "mass": 53 + }, + "python_nx_armour_reactive": { + "mass": 53 + }, "sidewinder_armour_grade1": { "mass": 0 }, From 6032e5202d74d30cc187e467fd1cbc49e6e5f2a4 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Sat, 11 May 2024 17:58:39 -0400 Subject: [PATCH 32/33] [RELEASE] 5.10.6 --- ChangeLog.md | 29 +++++++++++++++++++++++++++++ config/__init__.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 324d1f8d..1dc357f1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,27 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in the source (not distributed with the Windows installer) for the currently used version. --- +Release 5.10.6 +=== +This release contains the data information for the new SCO modules added in Elite update 18.04. +This should represent full support for the new Python Mk II. + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Added new SCO Module Details +* Reverted a change from the prior release due to breaking some consumers. +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + Release 5.10.5 === This release contains a fix for a bug that could crash EDMC's console versions when reading outfitting information @@ -14,6 +35,9 @@ from the new SCO Frame Shift Drive modules. Please note that this does not offer full support for the new SCO modules or the Python Mk II. More support will be added in a future update. +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + **Changes and Enhancements** * Updated Translations * Added limited data regarding the Python Mk II @@ -24,6 +48,11 @@ be added in a future update. * Fixed a bug where the new SCO modules would display as a normal Frame Shift Drive * Fixed a bug which could crash EDMC if the exact details of a Frame Shift Drive were unknown +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + Release 5.10.4 === This release contains updated dependencies, modules files, translations, and adds two new EDDN schemas. It also diff --git a/config/__init__.py b/config/__init__.py index a4a1e200..7e81d968 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -53,7 +53,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.10.5' +_static_appversion = '5.10.6' _cached_version: semantic_version.Version | None = None copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD' From fb6206279a4ba47e171ee12feeba00adc05f9740 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 May 2024 12:27:39 +0000 Subject: [PATCH 33/33] updating submodules --- coriolis-data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coriolis-data b/coriolis-data index 8adfd86b..651aab5a 160000 --- a/coriolis-data +++ b/coriolis-data @@ -1 +1 @@ -Subproject commit 8adfd86b64e8c14e873d2f5123d88ca6743420b9 +Subproject commit 651aab5af6a22980a1f88dcbb9ed256244cd6dff