From f9efe0e5a90d091b02f5a56132e3b394590fa6bb Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 4 Jun 2021 12:18:00 +0200 Subject: [PATCH 01/89] Fixed erroring on empty backpack.json closes EDCD/EDMarketConnector#1138 --- monitor.py | 70 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/monitor.py b/monitor.py index eff8799f..ae6395d6 100644 --- a/monitor.py +++ b/monitor.py @@ -1,6 +1,7 @@ """Monitor for new Journal files and contents of latest.""" import json +import pathlib import queue import re import threading @@ -880,43 +881,54 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below elif event_type in ('BackPack', 'Backpack'): # WORKAROUND 4.0.0.200: BackPack becomes Backpack # TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley # said it's `Backpack.json` - with open(join(self.currentdir, 'Backpack.json'), 'rb') as backpack: # type: ignore + backpack_file = pathlib.Path(str(self.currentdir)) / 'Backpack.json' + backpack_data = None + if backpack_file.exists(): + backpack_data = backpack_file.read_bytes() + else: + logger.warning(f'Failed to find backpack.json file as it appears not to exist? {backpack_file=}') + + parsed = None + + if backpack_data is not None and len(backpack_data) > 0: try: - # Preserve property order because why not? - entry = json.load(backpack, object_pairs_hook=OrderedDict) - + parsed = json.loads(backpack_data) except json.JSONDecodeError: - logger.exception('Failed decoding Backpack.json', exc_info=True) + logger.exception('Unable to parse Backpack.json') - else: - # Store in monitor.state - self.state['BackpackJSON'] = entry + else: + logger.warning('Unable to read backpack data! Either the file was empty or it didn\'t exist!') - # Assume this reflects the current state when written - self.state['BackPack']['Component'] = defaultdict(int) - self.state['BackPack']['Consumable'] = defaultdict(int) - self.state['BackPack']['Item'] = defaultdict(int) - self.state['BackPack']['Data'] = defaultdict(int) + if parsed is not None: + entry = parsed # set entry so that it ends up in plugins with the right data + # Store in monitor.state + self.state['BackpackJSON'] = entry - clean_components = self.coalesce_cargo(entry['Components']) - self.state['BackPack']['Component'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_components} - ) + # Assume this reflects the current state when written + self.state['BackPack']['Component'] = defaultdict(int) + self.state['BackPack']['Consumable'] = defaultdict(int) + self.state['BackPack']['Item'] = defaultdict(int) + self.state['BackPack']['Data'] = defaultdict(int) - clean_consumables = self.coalesce_cargo(entry['Consumables']) - self.state['BackPack']['Consumable'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_consumables} - ) + clean_components = self.coalesce_cargo(entry['Components']) + self.state['BackPack']['Component'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_components} + ) - clean_items = self.coalesce_cargo(entry['Items']) - self.state['BackPack']['Item'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_items} - ) + clean_consumables = self.coalesce_cargo(entry['Consumables']) + self.state['BackPack']['Consumable'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_consumables} + ) - clean_data = self.coalesce_cargo(entry['Data']) - self.state['BackPack']['Data'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_data} - ) + clean_items = self.coalesce_cargo(entry['Items']) + self.state['BackPack']['Item'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_items} + ) + + clean_data = self.coalesce_cargo(entry['Data']) + self.state['BackPack']['Data'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_data} + ) elif event_type == 'BackpackChange': # Changes to Odyssey Backpack contents *other* than from a Transfer From bf4e89377b3eb1ce934784b0fd376e0fe6e5d535 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 4 Jun 2021 14:40:53 +0200 Subject: [PATCH 02/89] Fixed line numbers for test --- tests/EDMCLogging.py/test_logging_classvar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/EDMCLogging.py/test_logging_classvar.py b/tests/EDMCLogging.py/test_logging_classvar.py index 3bedb0f5..24ab009e 100644 --- a/tests/EDMCLogging.py/test_logging_classvar.py +++ b/tests/EDMCLogging.py/test_logging_classvar.py @@ -40,6 +40,6 @@ def test_class_logger(caplog: 'LogCaptureFixture') -> None: log_stuff('test3') # type: ignore # its there # Dont move these, it relies on the line numbres. - assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:37 test' in caplog.text - assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:38 test2' in caplog.text - assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:25 test3' in caplog.text + assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:38 test' in caplog.text + assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:39 test2' in caplog.text + assert 'EDMarketConnector.EDMCLogging.py:test_logging_classvar.py:26 test3' in caplog.text From caf594752f10376ee00516e94b919b423156f361 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 4 Jun 2021 16:26:07 +0200 Subject: [PATCH 03/89] Made use of guard clauses where possible --- monitor.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/monitor.py b/monitor.py index ae6395d6..752026f8 100644 --- a/monitor.py +++ b/monitor.py @@ -883,21 +883,27 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # said it's `Backpack.json` backpack_file = pathlib.Path(str(self.currentdir)) / 'Backpack.json' backpack_data = None - if backpack_file.exists(): - backpack_data = backpack_file.read_bytes() - else: + + if not backpack_file.exists(): logger.warning(f'Failed to find backpack.json file as it appears not to exist? {backpack_file=}') + else: + backpack_data = backpack_file.read_bytes() + parsed = None - if backpack_data is not None and len(backpack_data) > 0: - try: - parsed = json.loads(backpack_data) - except json.JSONDecodeError: - logger.exception('Unable to parse Backpack.json') + if backpack_data is None: + logger.warning('Unable to read backpack data!') + + elif len(backpack_data) == 0: + logger.warning('Backpack.json was empty when we read it!') else: - logger.warning('Unable to read backpack data! Either the file was empty or it didn\'t exist!') + try: + parsed = json.loads(backpack_data) + + except json.JSONDecodeError: + logger.exception('Unable to parse Backpack.json') if parsed is not None: entry = parsed # set entry so that it ends up in plugins with the right data From ce9e7985c710dc89e0557b8aa3954d90e6a9ad99 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 4 Jun 2021 19:26:31 +0100 Subject: [PATCH 04/89] Release 5.1.0: Post-release bump of appversion to next-beta0 --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 99295346..94843654 100644 --- a/config.py +++ b/config.py @@ -33,7 +33,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.1.0' +_static_appversion = '5.1.1-beta0' copyright = '© 2015-2019 Jonathan Harris, 2020-2021 EDCD' update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' From 09da4576bf4d3056138640404369a134ad717c65 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 5 Jun 2021 08:54:55 +0100 Subject: [PATCH 05/89] plugins/coriolis: Correct case of translated string --- plugins/coriolis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/coriolis.py b/plugins/coriolis.py index f98b0105..c276eff4 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -80,7 +80,7 @@ def plugin_prefs(parent: tk.Widget, cmdr: str, is_beta: bool) -> tk.Frame: ) cur_row += 1 - nb.Label(conf_frame, text=_('Override Beta/Normal selection')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX) + nb.Label(conf_frame, text=_('Override Beta/Normal Selection')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX) nb.OptionMenu( conf_frame, override_textvar, From 73ead7c4ef7f5e09c412d0d65ada5c2f93110e2f Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 5 Jun 2021 09:19:47 +0100 Subject: [PATCH 06/89] Translations: Add generic 'Check', as in 'Check E:D journal file location' --- EDMarketConnector.py | 2 +- L10n/en.template | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index f6e17e87..59120a89 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -694,7 +694,7 @@ class AppWindow(object): # (Re-)install log monitoring if not monitor.start(self.w): - self.status['text'] = f'Error: Check {_("E:D journal file location")}' + self.status['text'] = f'Error: {_("Check")} {_("E:D journal file location")}' if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr diff --git a/L10n/en.template b/L10n/en.template index ef573b64..b6afcaae 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -84,6 +84,9 @@ /* Folder selection button on OSX. [prefs.py] */ "Change..." = "Change..."; +/* Generic 'Check', as in 'Check E:D journal file location' [EDMarketConnector.py] */ +"Check" = "Check"; + /* Menu item. [EDMarketConnector.py] */ "Check for Updates..." = "Check for Updates..."; From 5acb5e614000e091e3e0359866f8ca4dbeffa93d Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 5 Jun 2021 09:20:17 +0100 Subject: [PATCH 07/89] EDMarketConnector: `import update` needed for type checking --- EDMarketConnector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 59120a89..d757c905 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -263,7 +263,7 @@ if __name__ == '__main__': # noqa: C901 # isort: off if TYPE_CHECKING: from logging import TRACE # type: ignore # noqa: F401 # Needed to update mypy - # import update + import update # from infi.systray import SysTrayIcon # isort: on From f6f5a8a8feb57787f0098521a6c6791d42f998bd Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 7 Jun 2021 11:03:41 +0100 Subject: [PATCH 08/89] Contributing: Fix header levels after 'Scope changes' --- Contributing.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Contributing.md b/Contributing.md index 5b4518f1..6257fea3 100644 --- a/Contributing.md +++ b/Contributing.md @@ -265,12 +265,12 @@ No: return ``` -### Use Type hints +## Use Type hints Please do place [type hints](https://docs.python.org/3/library/typing.html) on the declarations of your functions, both their arguments and return types. -### Use `logging` not `print()`, and definitely not `sys.stdout.write()` +## Use `logging` not `print()`, and definitely not `sys.stdout.write()` `EDMarketConnector.py` sets up a `logging.Logger` for this under the `appname`, so: @@ -312,7 +312,7 @@ exception*. e.g. Logging will know you were in your `get_foo()` function but you should still tell it what actually (failed to have) happened in there. -### Prefer fstrings to modulo-formatting and .format +## Prefer fstrings to modulo-formatting and .format [fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, and allow for string interpolation rather than more opaque formatting calls. @@ -320,12 +320,12 @@ than more opaque formatting calls. As part of our flake8 linting setup we have included a linter that warns when you use either `%` or `.format` on string literals. -### Docstrings +## Docstrings Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code. We use the `sphinx` formatting style, which for pycharm users is the default. -### Mark hacks and workarounds with a specific comment +## Mark hacks and workarounds with a specific comment We often write hacks or workarounds to make EDMC work on a given version or around a specific bug. Please mark all hacks, workarounds, magic with one of the following comments, where applicable: From 3975aa3e87b21b5b7e007dfd303ff1a43dcd661a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 7 Jun 2021 11:16:02 +0100 Subject: [PATCH 09/89] Contributing: Choose an appropriate logging level Open to comments on my definitions. --- Contributing.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Contributing.md b/Contributing.md index 6257fea3..3a296798 100644 --- a/Contributing.md +++ b/Contributing.md @@ -312,6 +312,43 @@ exception*. e.g. Logging will know you were in your `get_foo()` function but you should still tell it what actually (failed to have) happened in there. +### Use the appropriate logging level +You must ensure necessary information is always in the log files, but +not so much that it becomes more difficult to discern important information +when diagnosing an issue. + +`logging`, and thus our `logger` instances provide functions of the +following names: + +- `info` - For general messages that don't occur too often outside of startup + and shutdown. +- `warning` - An error has been detected, but it doesn't impact continuing + functionality. In particular **use this when logging errors from + external services**. This would include where we detected a known issue + with Frontier-supplied data. A currently unknown issue *may* end up + triggering logging at `error` level or above. +- `error` - An error **in our code** has occurred. The application might be + able to continue, but we want to make it obvious there's a bug that we + need to fix. +- `critical` - An error has occurred **in our code** that impacts the + continuation of the current process. +- `debug` - Information about code flow and data that is occurs too often + to be at `info` level. Keep in mind our *default* logging level is DEBUG, + but users can change it for the + [plain log file](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#plain-log-file), + but the + [debug log giles](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files) + are always at least at DEBUG level. + +In addition to that we utilise one of the user-defined levels as: + +- `trace` - This is a custom log level intended for debug messages which + occur even more often and would cause too much log output for even + 'normal' debug use. + In general only developers will set this log level, but we do supply a + command-line argument and `.bat` file for users to enable it. It cannot be + selected from Settings in the UI. + ## Prefer fstrings to modulo-formatting and .format [fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, and allow for string interpolation rather From 22811248074fa11f8f1851cf640742b070a49469 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 00:39:29 +0200 Subject: [PATCH 10/89] Added AST based LANG translation string detection --- scripts/find_localised_strings.py | 129 ++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 scripts/find_localised_strings.py diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py new file mode 100644 index 00000000..10f6fc0e --- /dev/null +++ b/scripts/find_localised_strings.py @@ -0,0 +1,129 @@ +"""Search all given paths recursively for localised string calls.""" +import argparse +import ast +import json +import pathlib + + +def get_func_name(thing: ast.AST) -> str: + """Get the name of a function from a Call node.""" + if isinstance(thing, ast.Name): + return thing.id + + elif isinstance(thing, ast.Attribute): + return get_func_name(thing.value) + + else: + return "" + + +def get_arg(call: ast.Call) -> str: + """Extract the argument string to the translate function.""" + if len(call.args) > 1: + print("??? > 1 args", call.args) + + arg = call.args[0] + if isinstance(arg, ast.Constant): + return arg.value + else: + return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}' + + +def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: + """Recursively find ast.Calls in a statement.""" + 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) + + return out + + +def scan_file(path: pathlib.Path) -> list[ast.Call]: + """Scan a file for ast.Calls.""" + data = path.read_text() + parsed = ast.parse(data) + out: list[ast.Call] = [] + + for statement in parsed.body: + out.extend(find_calls_in_stmt(statement)) + + out.sort(key=lambda c: c.lineno) + return out + + +def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] = None) -> dict[pathlib.Path, list[ast.Call]]: + """ + Scan a directory for expected callsites. + + :param path: path to scan + :param skip: paths to skip, if any, defaults to None + """ + out = {} + for thing in path.iterdir(): + if skip is not None and any(s.name == thing.name for s in skip): + continue + + if thing.is_file(): + if not thing.name.endswith('.py'): + continue + + out[thing] = scan_file(thing) + + elif thing.is_dir(): + out |= scan_directory(thing) + + else: + raise ValueError(type(thing), thing) + + return out + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--directory", help="Directory to search from", default=".") + parser.add_argument("--ignore", action='append', help="directories to ignore (globbed)", default=["venv", ".git"]) + group = parser.add_mutually_exclusive_group() + group.add_argument('--json', action='store_true', help='JSON output') + group.add_argument('--lang', action='store_true', help='lang file outpot') + + args = parser.parse_args() + + directory = pathlib.Path(args.directory) + res = scan_directory(directory, [pathlib.Path(p) for p in args.ignore]) + + if args.json: + to_print = json.dumps({ + str(path): [{ + "string": get_arg(c), + "reconstructed": ast.unparse(c), + "start_line": c.lineno, + "start_offset": c.col_offset, + "end_line": c.end_lineno, + "end_offset": c.end_col_offset, + } for c in calls] for (path, calls) in res.items() if len(calls) > 0 + }, indent=2) + + print(to_print) + + elif args.lang: + for path, calls in res.items(): + for c in calls: + arg = json.dumps(get_arg(c)) + print(f'/* {path.name}:{c.lineno}({c.col_offset}):{c.end_lineno}({c.end_col_offset}) */') + print(f'{arg} = {arg};') + print() + + else: + for path, calls in res.items(): + if len(calls) == 0: + continue + + print(path) + for c in calls: + print( + f" {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t", ast.unparse(c) + ) + + print() From ce8fea9f1a3a39c4895323182defc12324a1fd81 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 11:45:36 +0200 Subject: [PATCH 11/89] its not globbed --- scripts/find_localised_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 10f6fc0e..de2d12c1 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -83,7 +83,7 @@ def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] = None) -> dict[ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--directory", help="Directory to search from", default=".") - parser.add_argument("--ignore", action='append', help="directories to ignore (globbed)", default=["venv", ".git"]) + parser.add_argument("--ignore", action='append', help="directories to ignore", default=["venv", ".git"]) group = parser.add_mutually_exclusive_group() group.add_argument('--json', action='store_true', help='JSON output') group.add_argument('--lang', action='store_true', help='lang file outpot') From b19f956219545107f951592170972dca9850c538 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 11:53:25 +0200 Subject: [PATCH 12/89] use stderr --- scripts/find_localised_strings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index de2d12c1..1957b32e 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -1,4 +1,5 @@ """Search all given paths recursively for localised string calls.""" +import sys import argparse import ast import json @@ -20,7 +21,7 @@ def get_func_name(thing: ast.AST) -> str: def get_arg(call: ast.Call) -> str: """Extract the argument string to the translate function.""" if len(call.args) > 1: - print("??? > 1 args", call.args) + print("??? > 1 args", call.args, file=sys.stderr) arg = call.args[0] if isinstance(arg, ast.Constant): From 5bdbf334ae652a8bc41d307107f1f065c3f93093 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 12:27:06 +0200 Subject: [PATCH 13/89] Remove hack --- EDMarketConnector.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index d757c905..ff76113c 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1721,18 +1721,15 @@ sys.path: {sys.path}''' """Display message about plugins not updated for Python 3.x.""" plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0) if (plugins_not_py3_last + 86400) < int(time()) and len(plug.PLUGINS_not_py3): - # Yes, this is horribly hacky so as to be sure we match the key - # that we told Translators to use. - popup_text = "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." - popup_text = popup_text.replace('\n', '\\n') - popup_text = popup_text.replace('\r', '\\r') - # Now the string should match, so try translation - popup_text = _(popup_text) - # And substitute in the other words. + popup_text = _( + "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 " + r"Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on " + "the end of the name." + ) + + # Substitute in the other words. popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled') # And now we do need these to be actual \r\n From dbea29194bcb895bead32be2474750879b96b4c9 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 12:27:50 +0200 Subject: [PATCH 14/89] Added the ability to compare to another LANG file --- scripts/find_localised_strings.py | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 1957b32e..87c19825 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -1,6 +1,8 @@ """Search all given paths recursively for localised string calls.""" +import re import sys import argparse +import itertools import ast import json import pathlib @@ -26,6 +28,8 @@ def get_arg(call: ast.Call) -> str: arg = call.args[0] if isinstance(arg, ast.Constant): return arg.value + elif isinstance(arg, ast.Name): + return f"VARIABLE! CHECK CODE! {arg.id}" else: return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}' @@ -81,6 +85,19 @@ def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] = None) -> dict[ return out +def parse_template(path) -> set[str]: + lang_re = re.compile(r'\s*"((?:[^"]|(?:\"))+)"\s*=\s*"((?:[^"]|(?:\"))+)"\s*;\s*$') + out = set() + for line in pathlib.Path(path).read_text().splitlines(): + match = lang_re.match(line) + if not match: + continue + if match.group(1) != "!Language": + out.add(match.group(1)) + + return out + + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--directory", help="Directory to search from", default=".") @@ -88,13 +105,29 @@ if __name__ == '__main__': group = parser.add_mutually_exclusive_group() group.add_argument('--json', action='store_true', help='JSON output') group.add_argument('--lang', action='store_true', help='lang file outpot') + group.add_argument('--compare-lang') args = parser.parse_args() directory = pathlib.Path(args.directory) res = scan_directory(directory, [pathlib.Path(p) for p in args.ignore]) - if args.json: + if args.compare_lang is not None and len(args.compare_lang) > 0: + seen = set() + template = parse_template(args.compare_lang) + + for file, calls in res.items(): + for c in calls: + arg = get_arg(c) + if arg in template: + seen.add(arg) + else: + print(f"NEW! {file}:{c.lineno}: {arg!r}") + + for old in set(template) ^ seen: + print(f"No longer used: {old}") + + elif args.json: to_print = json.dumps({ str(path): [{ "string": get_arg(c), From 0c5806e12ce6461ccd048dd6626450acc09f2e05 Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 12:35:31 +0200 Subject: [PATCH 15/89] Made json output easier to work with --- scripts/find_localised_strings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 87c19825..6516b720 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -128,18 +128,19 @@ if __name__ == '__main__': print(f"No longer used: {old}") elif args.json: - to_print = json.dumps({ - str(path): [{ + to_print_data = [ + { + "path": str(path), "string": get_arg(c), "reconstructed": ast.unparse(c), "start_line": c.lineno, "start_offset": c.col_offset, "end_line": c.end_lineno, "end_offset": c.end_col_offset, - } for c in calls] for (path, calls) in res.items() if len(calls) > 0 - }, indent=2) + } for (path, calls) in res.items() for c in calls + ] - print(to_print) + print(json.dumps(to_print_data, indent=2)) elif args.lang: for path, calls in res.items(): From 9c4bc182ca2c981894fbda50713812887afc44be Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 13:47:44 +0200 Subject: [PATCH 16/89] Added LANG file generation and comment extraction --- Contributing.md | 9 +- scripts/find_localised_strings.py | 132 ++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/Contributing.md b/Contributing.md index 3a296798..4f77cd7f 100644 --- a/Contributing.md +++ b/Contributing.md @@ -362,7 +362,14 @@ literals. Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code. We use the `sphinx` formatting style, which for pycharm users is the default. -## Mark hacks and workarounds with a specific comment +### Add comments to LANG usage + +Sometimes our translators may need some additional information about what a translation is used for. You can add +that information automatically by using `# LANG: your message here` +**on the line directly above your usage, or at the end of the line in your usage**. If both comments exist, the one +on the current line is preferred over the one above + +### Mark hacks and workarounds with a specific comment We often write hacks or workarounds to make EDMC work on a given version or around a specific bug. Please mark all hacks, workarounds, magic with one of the following comments, where applicable: diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 6516b720..a4decb41 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -1,11 +1,12 @@ """Search all given paths recursively for localised string calls.""" -import re -import sys import argparse -import itertools import ast import json import pathlib +import re +import sys +import dataclasses +from typing import Optional def get_func_name(thing: ast.AST) -> str: @@ -40,20 +41,64 @@ def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: 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) return out +COMMENT_RE = re.compile(r'^.*?(#.*)$') + + +def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Optional[str]: + out: list[Optional[str]] = [] + above = call.lineno - 2 + current = call.lineno - 1 + + above_line = lines[above].strip() if len(lines) < above else None + current_line = lines[current].strip() + + for line in (above_line, current_line): + if line is None or '#' not in line: + out.append(None) + continue + + match = COMMENT_RE.match(line) + if not match: + print(line) + out.append(None) + continue + + comment = match.group(1).strip() + if not comment.startswith("# LANG:"): + print(f'Unknown comment for {file}:{current} {line}', file=sys.stderr) + out.append(None) + continue + + out.append(comment.replace("# LANG:", "").strip()) + + if out[1] is not None: + return out[1] + elif out[0] is not None: + return out[0] + + return None + + def scan_file(path: pathlib.Path) -> list[ast.Call]: """Scan a file for ast.Calls.""" data = path.read_text() + lines = data.splitlines() parsed = ast.parse(data) out: list[ast.Call] = [] for statement in parsed.body: out.extend(find_calls_in_stmt(statement)) + # see if we can extract any comments + for call in out: + setattr(call, "comment", extract_comments(call, lines, path)) + out.sort(key=lambda c: c.lineno) return out @@ -98,6 +143,79 @@ def parse_template(path) -> set[str]: return out +@dataclasses.dataclass +class FileLocation: + path: pathlib.Path + line_start: int + line_start_col: int + line_end: Optional[int] + line_end_col: Optional[int] + + @staticmethod + def from_call(path: pathlib.Path, c: ast.Call) -> 'FileLocation': + return FileLocation(path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset) + + +@dataclasses.dataclass +class LangEntry: + locations: list[FileLocation] + string: str + comments: list[Optional[str]] + + def files(self) -> str: + out = "" + for loc in self.locations: + start = loc.line_start + end = loc.line_end + end_str = f':{end}' if end is not None and end != start else '' + out += f'{loc.path.name}:{start}{end_str}; ' + + return out + + +def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]): + """Generate a full en.template from the given data.""" + entries: list[LangEntry] = [] + for path, calls in data.items(): + for c in calls: + entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) + + deduped: list[LangEntry] = [] + for e in entries: + cont = False + for d in deduped: + if d.string == e.string: + cont = True + d.locations.append(e.locations[0]) + d.comments.extend(e.comments) + + if cont: + continue + + deduped.append(e) + + print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) + for entry in deduped: + assert len(entry.comments) == len(entry.locations) + comment = "" + files = "In files: " + entry.files() + string = f'"{entry.string}"' + + for i in range(len(entry.comments)): + if entry.comments[i] is None: + continue + + loc = entry.locations[i] + to_append = f'{loc.path.name}: {entry.comments[i]}; ' + if to_append not in comment: + comment += to_append + + header = f'{comment.strip()} {files}'.strip() + print(f'/* {header} */') + print(f'{string} = {string};') + print() + + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--directory", help="Directory to search from", default=".") @@ -137,18 +255,14 @@ if __name__ == '__main__': "start_offset": c.col_offset, "end_line": c.end_lineno, "end_offset": c.end_col_offset, + "comment": getattr(c, "comment", None) } for (path, calls) in res.items() for c in calls ] print(json.dumps(to_print_data, indent=2)) elif args.lang: - for path, calls in res.items(): - for c in calls: - arg = json.dumps(get_arg(c)) - print(f'/* {path.name}:{c.lineno}({c.col_offset}):{c.end_lineno}({c.end_col_offset}) */') - print(f'{arg} = {arg};') - print() + print(generate_lang_template(res)) else: for path, calls in res.items(): From 398bc76056c76a9f2f8778cc5508aebf701f96ad Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 6 Jun 2021 13:53:33 +0200 Subject: [PATCH 17/89] returned string rather than printing --- scripts/find_localised_strings.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index a4decb41..3a1f4425 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -7,6 +7,7 @@ import re import sys import dataclasses from typing import Optional +import io def get_func_name(thing: ast.AST) -> str: @@ -173,7 +174,7 @@ class LangEntry: return out -def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]): +def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: """Generate a full en.template from the given data.""" entries: list[LangEntry] = [] for path, calls in data.items(): @@ -194,6 +195,7 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]): deduped.append(e) + out = "" print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) for entry in deduped: assert len(entry.comments) == len(entry.locations) @@ -211,9 +213,11 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]): comment += to_append header = f'{comment.strip()} {files}'.strip() - print(f'/* {header} */') - print(f'{string} = {string};') - print() + out += f'/* {header} */\n' + out += f'{string} = {string};\n' + out += "\n" + + return out if __name__ == '__main__': From 7d77e4586b5f8c56964d098f6659ba455dd8028d Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Jun 2021 12:27:38 +0200 Subject: [PATCH 18/89] Appeased the flake8 gods --- scripts/find_localised_strings.py | 64 ++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 3a1f4425..00746aa9 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -1,13 +1,14 @@ """Search all given paths recursively for localised string calls.""" import argparse import ast +import dataclasses import json import pathlib import re import sys -import dataclasses from typing import Optional -import io + +# spell-checker: words dedupe deduping deduped def get_func_name(thing: ast.AST) -> str: @@ -52,6 +53,17 @@ COMMENT_RE = re.compile(r'^.*?(#.*)$') def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Optional[str]: + """ + Extract comments from source code based on the given call. + + This returns comments on the same line as the call preferentially to comments above. + All comments must be prefixed with LANG, ie `# LANG: ` + + :param call: The call node to look for comments around. + :param lines: The file that the call node came from, as a list of strings where each string is a line. + :param file: The path to the file this call node came from + :return: The first comment that matches the rules, or None + """ out: list[Optional[str]] = [] above = call.lineno - 2 current = call.lineno - 1 @@ -88,7 +100,7 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op def scan_file(path: pathlib.Path) -> list[ast.Call]: """Scan a file for ast.Calls.""" - data = path.read_text() + data = path.read_text(encoding='utf-8') lines = data.splitlines() parsed = ast.parse(data) out: list[ast.Call] = [] @@ -132,9 +144,16 @@ def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] = None) -> dict[ def parse_template(path) -> set[str]: + """ + Parse a lang.template file. + + The regexp this uses was extracted from l10n.py. + + :param path: The path to the lang file + """ lang_re = re.compile(r'\s*"((?:[^"]|(?:\"))+)"\s*=\s*"((?:[^"]|(?:\"))+)"\s*;\s*$') out = set() - for line in pathlib.Path(path).read_text().splitlines(): + for line in pathlib.Path(path).read_text(encoding='utf-8').splitlines(): match = lang_re.match(line) if not match: continue @@ -146,6 +165,8 @@ def parse_template(path) -> set[str]: @dataclasses.dataclass class FileLocation: + """FileLocation is the location of a given string in a file.""" + path: pathlib.Path line_start: int line_start_col: int @@ -154,16 +175,25 @@ class FileLocation: @staticmethod def from_call(path: pathlib.Path, c: ast.Call) -> 'FileLocation': + """ + Create a FileLocation from a Call and Path. + + :param path: Path to the file this FileLocation is in + :param c: Call object to extract line information from + """ return FileLocation(path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset) @dataclasses.dataclass class LangEntry: + """LangEntry is a single translation that may span multiple files or locations.""" + locations: list[FileLocation] string: str comments: list[Optional[str]] def files(self) -> str: + """Return a string representation of all the files this LangEntry is in, and its location therein.""" out = "" for loc in self.locations: start = loc.line_start @@ -174,13 +204,16 @@ class LangEntry: return out -def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: - """Generate a full en.template from the given data.""" - entries: list[LangEntry] = [] - for path, calls in data.items(): - for c in calls: - entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) +def dedupe_lang_entries(entries: list[LangEntry]) -> list[LangEntry]: + """ + Deduplicate a list of lang entries. + This will coalesce LangEntries that have that same string but differing files and comments into a single + LangEntry that cotains all comments and FileLocations + + :param entries: The list to deduplicate + :return: The deduplicated list + """ deduped: list[LangEntry] = [] for e in entries: cont = False @@ -195,6 +228,17 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: deduped.append(e) + return deduped + + +def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: + """Generate a full en.template from the given data.""" + entries: list[LangEntry] = [] + for path, calls in data.items(): + for c in calls: + entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) + + deduped = dedupe_lang_entries(entries) out = "" print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) for entry in deduped: From 18c1bb1102f391f37d1039b19d3ac8d3748d43b1 Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Jun 2021 12:36:05 +0200 Subject: [PATCH 19/89] Made quoting consistent --- scripts/find_localised_strings.py | 52 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 00746aa9..b62ddd78 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -20,19 +20,19 @@ def get_func_name(thing: ast.AST) -> str: return get_func_name(thing.value) else: - return "" + return '' def get_arg(call: ast.Call) -> str: """Extract the argument string to the translate function.""" if len(call.args) > 1: - print("??? > 1 args", call.args, file=sys.stderr) + print('??? > 1 args', call.args, file=sys.stderr) arg = call.args[0] if isinstance(arg, ast.Constant): return arg.value elif isinstance(arg, ast.Name): - return f"VARIABLE! CHECK CODE! {arg.id}" + return f'VARIABLE! CHECK CODE! {arg.id}' else: return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}' @@ -42,7 +42,7 @@ 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) == "_": + if isinstance(statement, ast.Call) and get_func_name(statement.func) == '_': out.append(statement) @@ -83,12 +83,12 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op continue comment = match.group(1).strip() - if not comment.startswith("# LANG:"): + if not comment.startswith('# LANG:'): print(f'Unknown comment for {file}:{current} {line}', file=sys.stderr) out.append(None) continue - out.append(comment.replace("# LANG:", "").strip()) + out.append(comment.replace('# LANG:', '').strip()) if out[1] is not None: return out[1] @@ -110,7 +110,7 @@ def scan_file(path: pathlib.Path) -> list[ast.Call]: # see if we can extract any comments for call in out: - setattr(call, "comment", extract_comments(call, lines, path)) + setattr(call, 'comment', extract_comments(call, lines, path)) out.sort(key=lambda c: c.lineno) return out @@ -157,7 +157,7 @@ def parse_template(path) -> set[str]: match = lang_re.match(line) if not match: continue - if match.group(1) != "!Language": + if match.group(1) != '!Language': out.add(match.group(1)) return out @@ -194,7 +194,7 @@ class LangEntry: def files(self) -> str: """Return a string representation of all the files this LangEntry is in, and its location therein.""" - out = "" + out = '' for loc in self.locations: start = loc.line_start end = loc.line_end @@ -239,12 +239,12 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) deduped = dedupe_lang_entries(entries) - out = "" + out = '' print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) for entry in deduped: assert len(entry.comments) == len(entry.locations) - comment = "" - files = "In files: " + entry.files() + comment = '' + files = 'In files: ' + entry.files() string = f'"{entry.string}"' for i in range(len(entry.comments)): @@ -259,15 +259,15 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: header = f'{comment.strip()} {files}'.strip() out += f'/* {header} */\n' out += f'{string} = {string};\n' - out += "\n" + out += '\n' return out if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument("--directory", help="Directory to search from", default=".") - parser.add_argument("--ignore", action='append', help="directories to ignore", default=["venv", ".git"]) + parser.add_argument('--directory', help='Directory to search from', default='.') + parser.add_argument('--ignore', action='append', help='directories to ignore', default=['venv', '.git']) group = parser.add_mutually_exclusive_group() group.add_argument('--json', action='store_true', help='JSON output') group.add_argument('--lang', action='store_true', help='lang file outpot') @@ -288,22 +288,22 @@ if __name__ == '__main__': if arg in template: seen.add(arg) else: - print(f"NEW! {file}:{c.lineno}: {arg!r}") + print(f'NEW! {file}:{c.lineno}: {arg!r}') for old in set(template) ^ seen: - print(f"No longer used: {old}") + print(f'No longer used: {old}') elif args.json: to_print_data = [ { - "path": str(path), - "string": get_arg(c), - "reconstructed": ast.unparse(c), - "start_line": c.lineno, - "start_offset": c.col_offset, - "end_line": c.end_lineno, - "end_offset": c.end_col_offset, - "comment": getattr(c, "comment", None) + 'path': str(path), + 'string': get_arg(c), + 'reconstructed': ast.unparse(c), + 'start_line': c.lineno, + 'start_offset': c.col_offset, + 'end_line': c.end_lineno, + 'end_offset': c.end_col_offset, + 'comment': getattr(c, 'comment', None) } for (path, calls) in res.items() for c in calls ] @@ -320,7 +320,7 @@ if __name__ == '__main__': print(path) for c in calls: print( - f" {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t", ast.unparse(c) + f' {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t', ast.unparse(c) ) print() From 2fb47377a308f22caa7690f913a7a5af91039637 Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Jun 2021 12:41:12 +0200 Subject: [PATCH 20/89] Added comments --- EDMarketConnector.py | 86 +++++++++---------- stats.py | 194 ++++++++++++++++++++++--------------------- 2 files changed, 144 insertions(+), 136 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index ff76113c..4fc058ff 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -414,7 +414,7 @@ class AppWindow(object): else: appitem.grid(columnspan=2, sticky=tk.EW) - # Update button in main window + # LANG: Update button in main window self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) @@ -626,7 +626,7 @@ class AppWindow(object): return if (suit := monitor.state.get('SuitCurrent')) is None: - self.suit['text'] = f'<{_("Unknown")}>' + self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit return suitname = suit['edmcName'] @@ -701,48 +701,49 @@ class AppWindow(object): def set_labels(self): """Set main window labels, e.g. after language change.""" - self.cmdr_label['text'] = _('Cmdr') + ':' # Main window + self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Main window # Multicrew role label in main window self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window - self.suit_label['text'] = _('Suit') + ':' # Main window - self.system_label['text'] = _('System') + ':' # Main window - self.station_label['text'] = _('Station') + ':' # Main window - self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window + self.suit_label['text'] = _('Suit') + ':' # LANG: Main window + self.system_label['text'] = _('System') + ':' # LANG: Main window + self.station_label['text'] = _('Station') + ':' # LANG: Main window + self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window if platform == 'darwin': - self.menubar.entryconfigure(1, label=_('File')) # Menu title - self.menubar.entryconfigure(2, label=_('Edit')) # Menu title - self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX - self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX - self.menubar.entryconfigure(5, label=_('Help')) # Menu title - self.system_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX - self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item - self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item - self.view_menu.entryconfigure(0, label=_('Status')) # Menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item + self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title on OSX + self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title on OSX + self.menubar.entryconfigure(3, label=_('View')) # LANG: Menu title on OSX + self.menubar.entryconfigure(4, label=_('Window')) # LANG: Menu title on OSX + self.menubar.entryconfigure(5, label=_('Help')) # LANG: Menu title on OSX + self.system_menu.entryconfigure(0, label=_("About {APP}").format( + APP=applongname)) # LANG: App menu entry on OSX + self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Menu item + self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: Menu item + self.view_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item else: - self.menubar.entryconfigure(1, label=_('File')) # Menu title - self.menubar.entryconfigure(2, label=_('Edit')) # Menu title - self.menubar.entryconfigure(3, label=_('Help')) # Menu title - self.theme_file_menu['text'] = _('File') # Menu title - self.theme_edit_menu['text'] = _('Edit') # Menu title - self.theme_help_menu['text'] = _('Help') # Menu title + self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title + self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title + self.menubar.entryconfigure(3, label=_('Help')) # LANG: Menu title + self.theme_file_menu['text'] = _('File') # LANG: Menu title + self.theme_edit_menu['text'] = _('Edit') # LANG: Menu title + self.theme_help_menu['text'] = _('Help') # LANG: Menu title # File menu - self.file_menu.entryconfigure(0, label=_('Status')) # Menu item - self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item - self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows - self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows + self.file_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item + self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: Menu item + self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: Item in the File menu on Windows + self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: Item in the File menu on Windows # Help menu - self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item - self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item - self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry + self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help menu item + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item + self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # LANG: Menu item + self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # LANG: App menu entry # Edit menu - self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste + self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: As in Copy and Paste def login(self): """Initiate CAPI/Frontier login and set other necessary state.""" @@ -1004,9 +1005,9 @@ class AppWindow(object): return { None: '', 'Idle': '', - 'FighterCon': _('Fighter'), # Multicrew role - 'FireCon': _('Gunner'), # Multicrew role - 'FlightCon': _('Helm'), # Multicrew role + 'FighterCon': _('Fighter'), # LANG: Multicrew role + 'FireCon': _('Gunner'), # LANG: Multicrew role + 'FlightCon': _('Helm'), # LANG: Multicrew role }.get(role, role) if monitor.thread is None: @@ -1024,7 +1025,7 @@ class AppWindow(object): self.cooldown() if monitor.cmdr and monitor.state['Captain']: self.cmdr['text'] = f'{monitor.cmdr} / {monitor.state["Captain"]}' - self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window + self.ship_label['text'] = _('Role') + ':' # LANG: Multicrew role label in main window self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None) elif monitor.cmdr: @@ -1034,7 +1035,7 @@ class AppWindow(object): else: self.cmdr['text'] = monitor.cmdr - self.ship_label['text'] = _('Ship') + ':' # Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: Main window # TODO: Show something else when on_foot if monitor.state['ShipName']: @@ -1057,7 +1058,7 @@ class AppWindow(object): else: self.cmdr['text'] = '' - self.ship_label['text'] = _('Ship') + ':' # Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: Main window self.ship['text'] = '' if monitor.cmdr and monitor.is_beta: @@ -1166,7 +1167,7 @@ class AppWindow(object): """ try: companion.session.auth_callback() - # Successfully authenticated with the Frontier website + # LANG: Successfully authenticated with the Frontier website self.status['text'] = _('Authentication successful') if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status @@ -1254,7 +1255,7 @@ class AppWindow(object): self.w.after(1000, self.cooldown) else: - self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window + self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window self.button['state'] = self.theme_button['state'] = (monitor.cmdr and monitor.mode and not monitor.state['Captain'] and @@ -1730,6 +1731,7 @@ sys.path: {sys.path}''' ) # Substitute in the other words. + # LANG: words for use in python 2 plugin error popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled') # And now we do need these to be actual \r\n diff --git a/stats.py b/stats.py index 704e4703..2eab96f2 100644 --- a/stats.py +++ b/stats.py @@ -47,117 +47,117 @@ def status(data: Dict[str, Any]) -> List[List[str]]: """ # StatsResults assumes these three things are first res = [ - [_('Cmdr'), data['commander']['name']], - [_('Balance'), str(data['commander'].get('credits', 0))], # Cmdr stats - [_('Loan'), str(data['commander'].get('debt', 0))], # Cmdr stats + [_('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 ] RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime # in output order - (_('Combat'), 'combat'), # Ranking - (_('Trade'), 'trade'), # Ranking - (_('Explorer'), 'explore'), # Ranking - (_('CQC'), 'cqc'), # Ranking - (_('Federation'), 'federation'), # Ranking - (_('Empire'), 'empire'), # Ranking - (_('Powerplay'), 'power'), # Ranking - # ??? , 'crime'), # Ranking - # ??? , 'service'), # Ranking + (_('Combat'), 'combat'), # LANG: Ranking + (_('Trade'), 'trade'), # LANG: Ranking + (_('Explorer'), 'explore'), # LANG: Ranking + (_('CQC'), 'cqc'), # LANG: Ranking + (_('Federation'), 'federation'), # LANG: Ranking + (_('Empire'), 'empire'), # LANG: Ranking + (_('Powerplay'), 'power'), # LANG: Ranking + # ??? , 'crime'), # LANG: Ranking + # ??? , 'service'), # LANG: Ranking ] RANK_NAMES = { # noqa: N806 # Its a constant, just needs to be updated at runtime # http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks 'combat': [ - _('Harmless'), # Combat rank - _('Mostly Harmless'), # Combat rank - _('Novice'), # Combat rank - _('Competent'), # Combat rank - _('Expert'), # Combat rank - _('Master'), # Combat rank - _('Dangerous'), # Combat rank - _('Deadly'), # Combat rank - _('Elite'), # Top rank + _('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 + _('Elite'), # LANG: Top rank ], 'trade': [ - _('Penniless'), # Trade rank - _('Mostly Penniless'), # Trade rank - _('Peddler'), # Trade rank - _('Dealer'), # Trade rank - _('Merchant'), # Trade rank - _('Broker'), # Trade rank - _('Entrepreneur'), # Trade rank - _('Tycoon'), # Trade rank - _('Elite') # Top rank + _('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 + _('Elite') # LANG: Top rank ], 'explore': [ - _('Aimless'), # Explorer rank - _('Mostly Aimless'), # Explorer rank - _('Scout'), # Explorer rank - _('Surveyor'), # Explorer rank - _('Trailblazer'), # Explorer rank - _('Pathfinder'), # Explorer rank - _('Ranger'), # Explorer rank - _('Pioneer'), # Explorer rank - _('Elite') # Top rank + _('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 + _('Elite') # LANG: Top rank ], 'cqc': [ - _('Helpless'), # CQC rank - _('Mostly Helpless'), # CQC rank - _('Amateur'), # CQC rank - _('Semi Professional'), # CQC rank - _('Professional'), # CQC rank - _('Champion'), # CQC rank - _('Hero'), # CQC rank - _('Gladiator'), # CQC rank - _('Elite') # Top rank + _('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 + _('Elite') # LANG: Top rank ], # http://elite-dangerous.wikia.com/wiki/Federation#Ranks 'federation': [ - _('None'), # No rank - _('Recruit'), # Federation rank - _('Cadet'), # Federation rank - _('Midshipman'), # Federation rank - _('Petty Officer'), # Federation rank - _('Chief Petty Officer'), # Federation rank - _('Warrant Officer'), # Federation rank - _('Ensign'), # Federation rank - _('Lieutenant'), # Federation rank - _('Lieutenant Commander'), # Federation rank - _('Post Commander'), # Federation rank - _('Post Captain'), # Federation rank - _('Rear Admiral'), # Federation rank - _('Vice Admiral'), # Federation rank - _('Admiral') # Federation rank + _('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 ], # http://elite-dangerous.wikia.com/wiki/Empire#Ranks 'empire': [ - _('None'), # No rank - _('Outsider'), # Empire rank - _('Serf'), # Empire rank - _('Master'), # Empire rank - _('Squire'), # Empire rank - _('Knight'), # Empire rank - _('Lord'), # Empire rank - _('Baron'), # Empire rank - _('Viscount'), # Empire rank - _('Count'), # Empire rank - _('Earl'), # Empire rank - _('Marquis'), # Empire rank - _('Duke'), # Empire rank - _('Prince'), # Empire rank - _('King') # Empire rank + _('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 ], # http://elite-dangerous.wikia.com/wiki/Ratings 'power': [ - _('None'), # No rank - _('Rating 1'), # Power rank - _('Rating 2'), # Power rank - _('Rating 3'), # Power rank - _('Rating 4'), # Power rank - _('Rating 5') # Power rank + _('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 ], } @@ -169,7 +169,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')]) # No rank + res.append([title, _('None')]) # LANG: No rank return res @@ -291,7 +291,9 @@ class StatsDialog(): return if not data.get('commander') or not data['commander'].get('name', '').strip(): - self.status['text'] = _("Who are you?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown commander + self.status['text'] = _("Who are you?!") elif ( not data.get('lastSystem') @@ -299,10 +301,14 @@ class StatsDialog(): or not data.get('lastStarport') or not data['lastStarport'].get('name', '').strip() ): - self.status['text'] = _("Where are you?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown location + self.status['text'] = _("Where are you?!") elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip(): - self.status['text'] = _("What are you flying?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown ship + self.status['text'] = _("What are you flying?!") else: self.status['text'] = '' @@ -350,14 +356,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')) # Status dialog title + notebook.add(page, text=_('Status')) # LANG: Status dialog title page = self.addpage(notebook, [ - _('Ship'), # Status dialog subtitle + _('Ship'), # LANG: Status dialog subtitle '', - _('System'), # Main window - _('Station'), # Status dialog subtitle - _('Value'), # Status dialog subtitle - CR value of ship + _('System'), # LANG: Main window + _('Station'), # LANG: Status dialog subtitle + _('Value'), # LANG: Status dialog subtitle - CR value of ship ]) shiplist = ships(data) @@ -366,7 +372,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')) # Status dialog title + notebook.add(page, text=_('Ships')) # LANG: Status dialog title if platform != 'darwin': buttonframe = ttk.Frame(frame) From 6b7cb2a36639aa4ddaaa5f5bb91ddb7f1374ac5c Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Jun 2021 12:53:08 +0200 Subject: [PATCH 21/89] Fixed overindented titles --- Contributing.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Contributing.md b/Contributing.md index 4f77cd7f..4dc629ec 100644 --- a/Contributing.md +++ b/Contributing.md @@ -148,7 +148,7 @@ your WIP branch. If there are any non-trivial conflicts when we merge your Pull to rebase your WIP branch on the latest version of the source branch. Otherwise we'll work out how to best merge your changes via comments in the Pull Request. -### Linting +## Linting We use flake8 for linting all python source. @@ -158,7 +158,7 @@ your files. Note that if your PR does not cleanly (or mostly cleanly) pass a linting scan, your PR may be put on hold pending fixes. -### Unit testing +## Unit testing Where possible please write unit tests for your PRs, especially in the case of bug fixes, having regression tests help ensure that we don't accidentally @@ -362,6 +362,8 @@ literals. Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code. We use the `sphinx` formatting style, which for pycharm users is the default. +## Comments + ### Add comments to LANG usage Sometimes our translators may need some additional information about what a translation is used for. You can add @@ -369,11 +371,17 @@ that information automatically by using `# LANG: your message here` **on the line directly above your usage, or at the end of the line in your usage**. If both comments exist, the one on the current line is preferred over the one above +```py +# LANG: this says stuff. +_('stuff') +``` + ### Mark hacks and workarounds with a specific comment We often write hacks or workarounds to make EDMC work on a given version or around a specific bug. Please mark all hacks, workarounds, magic with one of the following comments, where applicable: -``` + +```py # HACK $elite-version-number | $date: $description # MAGIC $elite-version-number | $date: $description # WORKAROUND $elite-version-number | $date: $description From 3edf4ed069858d047d2873ecf1728c963a225b49 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 7 Jun 2021 12:04:14 +0100 Subject: [PATCH 22/89] Contributing: re-format 'Work on Issues' --- Contributing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 4dc629ec..0dd7c1d6 100644 --- a/Contributing.md +++ b/Contributing.md @@ -7,7 +7,10 @@ vim: textwidth=79 wrapmargin=79 If you are not part of the core development team then you should only be performing work that addresses an open issue. -So, if what you think needs doing isn't currently referred to in an [open issue](https://github.com/EDCD/EDMarketConnector/issues), then you should first [open an issue](https://github.com/EDCD/EDMarketConnector/issues/new/choose) **please use the correct template if applicable**. +So, if what you think needs doing isn't currently referred to in an +[open issue](https://github.com/EDCD/EDMarketConnector/issues), +then you should first [open an issue](https://github.com/EDCD/EDMarketConnector/issues/new/choose). +**Please use the correct template if applicable**. ## Check with us first From 54b57db329713289e8d4a1f3d0e853ee76b29f9b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 7 Jun 2021 12:10:43 +0100 Subject: [PATCH 23/89] Contributing: // now mandated --- Contributing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Contributing.md b/Contributing.md index 0dd7c1d6..f71d5673 100644 --- a/Contributing.md +++ b/Contributing.md @@ -129,12 +129,12 @@ Remember, you should always be working versus a single issue, even if the work i There might be cases where issues aren't duplicates, but your work still addresses more than one. In that case pick one for the naming scheme below, but mention all in commit messages and the Pull Request. -In all cases the branch should be named as per the scheme `<class>/<issue number>-<title>`: +In all cases the branch should be named as per the scheme `<class>/<issue number>/<title>`: * `<class>` - We have several classes of WIP branch: - * `fix` - For working on bug fixes, e.g. `fix/184-crash-in-startup` - * `enhancement` - For enhancing an *existing* feature, e.g. `enhancement/192-add-thing-to-wotsit` - * `feature` - For working on *new* features, e.g. `feature/284-allow-users-to-frob` + * `fix` - For working on bug fixes, e.g. `fix/184/crash-in-startup` + * `enhancement` - For enhancing an *existing* feature, e.g. `enhancement/192/add-thing-to-wotsit` + * `feature` - For working on *new* features, e.g. `feature/284/allow-users-to-frob` * `<issue-number>` is for easy reference when citing the issue number in commit messages. If you're somehow doing work that's not versus an issue then don't put the `<issue number>-` part in. From 9dcb79a5dbdea0e83304e2aedaeae3e4dc161dd1 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:11:58 +0100 Subject: [PATCH 24/89] Contributing: Minor grammar/punctuation --- Contributing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Contributing.md b/Contributing.md index f71d5673..b4547233 100644 --- a/Contributing.md +++ b/Contributing.md @@ -142,13 +142,13 @@ In all cases the branch should be named as per the scheme `<class>/<issue number succinct for `<title>`, it's just there for easy reference, it doesn't need to be the entire title of the appropriate issue. -Which branch you base your work on will depend on which class of WIP it is. If you're fixing a bug in the latest +The branch you base your work on will depend on which class of WIP it is. If you're fixing a bug in the latest `stable` then it's best to base your branch on its HEAD. If it's a fix for a beta release then base off of `beta`'s HEAD. If you're working on a new feature then you'd want to base the work on `develop`'s HEAD. **Important**: Please *under no circumstance* merge *from* the source branch after you have started work in your WIP branch. If there are any non-trivial conflicts when we merge your Pull Request then we might ask you -to rebase your WIP branch on the latest version of the source branch. Otherwise we'll work out how to best +to *rebase* your WIP branch on the latest version of the source branch. Otherwise, we'll work out how to best merge your changes via comments in the Pull Request. ## Linting From 3c13e84910599f4a4df4704b2727efdeb797f752 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:18:56 +0100 Subject: [PATCH 25/89] Contributing: Re-arrange sections more logically 1. General workflow first. 2. Then branches, including 'work in progress'. 3. Followed by tags. 4. *Then* 'Version conventions'. --- Contributing.md | 136 ++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/Contributing.md b/Contributing.md index b4547233..add45dbb 100644 --- a/Contributing.md +++ b/Contributing.md @@ -23,22 +23,25 @@ consistent with our vision for EDMC. Fundamental changes in particular need to b --- -## Version conventions +## General workflow -Please see [Version Strings](docs/Releasing.md#version-strings) -for a description of the currently used version strings. +1. You will need a GitHub account. +1. Fork the repository on GitHub into your account there (hereafter referred to as 'your fork'). +1. In your local copy of *your* fork create an appropriate WIP branch. +1. Develop the changes, testing as you go (no we don't have any actual tests yet). + 1. Be as sure as you can that the code works as you intend and hasn't introduced any other bugs or regressions. + 1. Test the codebase as a whole against any unit tests that do exist, and add your own as you can. + 1. Check your code against flake8 periodically. +1. When you're sure the work is final: + 1. Push your WIP branch to your fork (you probably should have been doing this as you worked as a form of backup). + 1. Access the WIP branch on your fork on GitHub and create a Pull Request. Mention any Issue number(s) that it + addresses. +1. Await feedback in the form of comments on the Pull Request. -Historically a `A.BC` form was used, based on an internal `A.B.C.D` version -string. This was changed to simply `A.B.C.D` throughout for `4.0.0.0`, -`4.0.1.0` and `4.0.2.0`. It would also continue for any other increment of -only the 'C' (Patch) component. - -Going forwards we will always use the full [Semantic Version](https://semver.org/#semantic-versioning-specification-semver) -and 'folder style' tag names, e.g. `Release/Major.Minor.Patch`. - -Currently the only file that defines the version code-wise is `config.py`. -`Changelog.md` and `edmarketconnector.xml` are another matter handled as part -of [the release process](docs/Releasing.md#distribution). +**IMPORTANT**: Once you have created the Pull Request *any changes you make to that WIP branch and push to your fork +will be reflected in the Pull Request*. Ensure that *only* the changes for the issue(s) you are addressing are in +the WIP branch. Any other work should occur in its own separate WIP branch. If needs be make one branch to work in +and another for the Pull Request, merging or cherry-picking commits as needed. --- @@ -46,15 +49,15 @@ of [the release process](docs/Releasing.md#distribution). Somewhat based on git-flow, but our particular take on it: -## Branches +### Branches -### `stable` +#### `stable` This will either have `HEAD` pointing to the latest stable release code *or* might have extra code merged in for a hotfix that will shortly be in the next stable release. If you want the latest stable release code then use the appropriate `Release/A.B.C` tag! -### `beta` +#### `beta` If we run any pre-release betas *with actual builds released, not just a branch to be run from source*, then this branch will contain that @@ -64,13 +67,13 @@ to be sure of the code you checkout. *If there hasn't yet been a new beta version this could be far behind all of: `main`, `develop`, `stable`.* -### `develop` +#### `develop` This is the branch where all current development is integrated. No commits should be made directly to this as the work should be done in a separate branch used in a Pull Request before being merged as part of resolving that Pull Request. -### `main` +#### `main` Yes, we've renamed this from `master`. See "[Using 'main' as the primary branch in Git](https://github.com/EDCD/EDMarketConnector/wiki/Git-Using-Main-Branch)" @@ -79,11 +82,11 @@ for instructions on ensuring you're cleanly using it in any local clone. This branch should contain anything from `develop` that is considered well tested and ready for the next `stable` merge. -### `master` +#### `master` **This is no longer used. If the branch is even present then it's no longer updated. You should be using `main` instead.** -### `releases` +#### `releases` Currently the version of the `edmarketconnector.xml` 'appcast' file in this branch is what live clients check to be notified of new versions. This can potentially be replaced with the `stable` branch's version, @@ -91,14 +94,42 @@ but some care will be necessary to ensure no users are left behind (their client then no longer exists). For the time being this should always be kept in sync with `stable` as each new release is made. -## Tags +### Work in progress conventions -### Stable Releases +Remember, you should always be working versus a single issue, even if the work is part of a Milestone or Project. +There might be cases where issues aren't duplicates, but your work still addresses more than one. In that case +pick one for the naming scheme below, but mention all in commit messages and the Pull Request. + +In all cases the branch should be named as per the scheme `<class>/<issue number>/<title>`: + +* `<class>` - We have several classes of WIP branch: + * `fix` - For working on bug fixes, e.g. `fix/184/crash-in-startup` + * `enhancement` - For enhancing an *existing* feature, e.g. `enhancement/192/add-thing-to-wotsit` + * `feature` - For working on *new* features, e.g. `feature/284/allow-users-to-frob` + +* `<issue-number>` is for easy reference when citing the issue number in commit messages. If you're somehow doing + work that's not versus an issue then don't put the `<issue number>-` part in. +* `<title>` is intended to allow anyone to quickly know what the branch is addressing. Try to choose something + succinct for `<title>`, it's just there for easy reference, it doesn't need to be the entire title of + the appropriate issue. + +The branch you base your work on will depend on which class of WIP it is. If you're fixing a bug in the latest +`stable` then it's best to base your branch on its HEAD. If it's a fix for a beta release then base off of `beta`'s +HEAD. If you're working on a new feature then you'd want to base the work on `develop`'s HEAD. + +**Important**: Please *under no circumstance* merge *from* the source branch after you have started work in +your WIP branch. If there are any non-trivial conflicts when we merge your Pull Request then we might ask you +to *rebase* your WIP branch on the latest version of the source branch. Otherwise, we'll work out how to best +merge your changes via comments in the Pull Request. + +### Tags + +#### Stable Releases All stable releases **MUST** have a tag of the form `Release/Major.Minor.Patch` on the commit that was `HEAD` when the installer for it was built. -### Pre-Releases +#### Pre-Releases Tags for pre-releases should be of one of two forms, following [Version Strings](docs/Releasing.md#version-strings) conventions. @@ -123,33 +154,24 @@ The Semantic Versioning `+<build metadata>` should never be a part of the tag. --- -## Work in progress conventions +## Version conventions -Remember, you should always be working versus a single issue, even if the work is part of a Milestone or Project. -There might be cases where issues aren't duplicates, but your work still addresses more than one. In that case -pick one for the naming scheme below, but mention all in commit messages and the Pull Request. +Please see [Version Strings](docs/Releasing.md#version-strings) +for a description of the currently used version strings. -In all cases the branch should be named as per the scheme `<class>/<issue number>/<title>`: +Historically a `A.BC` form was used, based on an internal `A.B.C.D` version +string. This was changed to simply `A.B.C.D` throughout for `4.0.0.0`, +`4.0.1.0` and `4.0.2.0`. It would also continue for any other increment of +only the 'C' (Patch) component. -* `<class>` - We have several classes of WIP branch: - * `fix` - For working on bug fixes, e.g. `fix/184/crash-in-startup` - * `enhancement` - For enhancing an *existing* feature, e.g. `enhancement/192/add-thing-to-wotsit` - * `feature` - For working on *new* features, e.g. `feature/284/allow-users-to-frob` +Going forwards we will always use the full [Semantic Version](https://semver.org/#semantic-versioning-specification-semver) +and 'folder style' tag names, e.g. `Release/Major.Minor.Patch`. -* `<issue-number>` is for easy reference when citing the issue number in commit messages. If you're somehow doing - work that's not versus an issue then don't put the `<issue number>-` part in. -* `<title>` is intended to allow anyone to quickly know what the branch is addressing. Try to choose something - succinct for `<title>`, it's just there for easy reference, it doesn't need to be the entire title of - the appropriate issue. +Currently the only file that defines the version code-wise is `config.py`. +`Changelog.md` and `edmarketconnector.xml` are another matter handled as part +of [the release process](docs/Releasing.md#distribution). -The branch you base your work on will depend on which class of WIP it is. If you're fixing a bug in the latest -`stable` then it's best to base your branch on its HEAD. If it's a fix for a beta release then base off of `beta`'s -HEAD. If you're working on a new feature then you'd want to base the work on `develop`'s HEAD. - -**Important**: Please *under no circumstance* merge *from* the source branch after you have started work in -your WIP branch. If there are any non-trivial conflicts when we merge your Pull Request then we might ask you -to *rebase* your WIP branch on the latest version of the source branch. Otherwise, we'll work out how to best -merge your changes via comments in the Pull Request. +--- ## Linting @@ -193,28 +215,6 @@ Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/cont --- -## General workflow - -1. You will need a GitHub account. -1. Fork the repository on GitHub into your account there (hereafter referred to as 'your fork'). -1. In your local copy of *your* fork create an appropriate WIP branch. -1. Develop the changes, testing as you go (no we don't have any actual tests yet). - 1. Be as sure as you can that the code works as you intend and hasn't introduced any other bugs or regressions. - 1. Test the codebase as a whole against any unit tests that do exist, and add your own as you can. - 1. Check your code against flake8 periodically. -1. When you're sure the work is final: - 1. Push your WIP branch to your fork (you probably should have been doing this as you worked as a form of backup). - 1. Access the WIP branch on your fork on GitHub and create a Pull Request. Mention any Issue number(s) that it - addresses. -1. Await feedback in the form of comments on the Pull Request. - -**IMPORTANT**: Once you have created the Pull Request *any changes you make to that WIP branch and push to your fork -will be reflected in the Pull Request*. Ensure that *only* the changes for the issue(s) you are addressing are in -the WIP branch. Any other work should occur in its own separate WIP branch. If needs be make one branch to work in -and another for the Pull Request, merging or cherry-picking commits as needed. - ---- - ## Coding Conventions ### In general, please follow [PEP8](https://www.python.org/dev/peps/pep-0008/) From 81652a54c2dc13c35546c9d11d353ccdfa019a21 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:21:00 +0100 Subject: [PATCH 26/89] Contributing: --- separators between each ## --- Contributing.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Contributing.md b/Contributing.md index add45dbb..ee1573bf 100644 --- a/Contributing.md +++ b/Contributing.md @@ -223,6 +223,8 @@ Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/cont Yes, this means using 'color' rather than 'colour', and in general will mean US, not British, spellings. +--- + ## Control flow Never oneline any control flow (`if`, `else`, `for`), as it makes spotting what happens next difficult. @@ -268,11 +270,15 @@ No: return ``` +--- + ## Use Type hints Please do place [type hints](https://docs.python.org/3/library/typing.html) on the declarations of your functions, both their arguments and return types. +--- + ## Use `logging` not `print()`, and definitely not `sys.stdout.write()` `EDMarketConnector.py` sets up a `logging.Logger` for this under the @@ -352,6 +358,8 @@ In addition to that we utilise one of the user-defined levels as: command-line argument and `.bat` file for users to enable it. It cannot be selected from Settings in the UI. +--- + ## Prefer fstrings to modulo-formatting and .format [fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, and allow for string interpolation rather @@ -360,11 +368,15 @@ than more opaque formatting calls. As part of our flake8 linting setup we have included a linter that warns when you use either `%` or `.format` on string literals. +--- + ## Docstrings Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code. We use the `sphinx` formatting style, which for pycharm users is the default. +--- + ## Comments ### Add comments to LANG usage From 93dbaee5ae5ac6b90f9c8169482fae7991023f6f Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:26:01 +0100 Subject: [PATCH 27/89] Contributing: "Git commit conventions" after "General workflow" Also linkified to the GitHub docs about 'Closes #XXX' and the like. --- Contributing.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Contributing.md b/Contributing.md index ee1573bf..19da9e15 100644 --- a/Contributing.md +++ b/Contributing.md @@ -45,6 +45,20 @@ and another for the Pull Request, merging or cherry-picking commits as needed. --- +## Git commit conventions + +* Please use the standard Git convention of a short title in the first line and fuller body text in subsequent lines. +* Please reference issue numbers using the "hashtag" format #123 in your commit message wherever possible. + This lets GitHub create two-way hyperlinks between the issue report and the commit. + [Certain text](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) + in a PR that fixes an issue can auto-close the issue when the PR is merged. + Note the caveats about the extended forms being necessary in some situations. +* If in doubt, lean towards many small commits. This makes git bisect much more useful. +* Please try at all costs to avoid a "mixed-up" commit, i.e. one that addresses more than one issue at once. + One thing at a time is best. + +--- + ## Git branch structure and tag conventions Somewhat based on git-flow, but our particular take on it: @@ -409,18 +423,6 @@ Additionally, if your hack is over around 5 lines, please include a `# HACK END` --- -## Git commit conventions - -* Please use the standard Git convention of a short title in the first line and fuller body text in subsequent lines. -* Please reference issue numbers using the "hashtag" format #123 in your commit message wherever possible. - This lets GitHub create two-way hyperlinks between the issue report and the commit. - Certain text in a PR that fixes an issue can auto-close the issue when the PR is merged. -* If in doubt, lean towards many small commits. This makes git bisect much more useful. -* Please try at all costs to avoid a "mixed-up" commit, i.e. one that addresses more than one issue at once. - One thing at a time is best. - ---- - ## Build process See [Releasing.md](docs/Releasing.md) for the environment and procedure necessary for building the application into From 522f15b0c6f77d7dc3c66df4a774df4d2965f8e9 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:27:45 +0100 Subject: [PATCH 28/89] Contributing: 'Coding Conventions' content shouldn't be headers --- Contributing.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Contributing.md b/Contributing.md index 19da9e15..79ccc8b8 100644 --- a/Contributing.md +++ b/Contributing.md @@ -231,11 +231,13 @@ Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/cont ## Coding Conventions -### In general, please follow [PEP8](https://www.python.org/dev/peps/pep-0008/) +In general, please follow [PEP8](https://www.python.org/dev/peps/pep-0008/) -### Adhere to the spelling conventions of the libraries and modules used in the project +Adhere to the spelling conventions of the libraries and modules used in the +project. -Yes, this means using 'color' rather than 'colour', and in general will mean US, not British, spellings. +Yes, this means using 'color' rather than 'colour', and in general will mean +US, not British, spellings. --- From e548dce4d84823014a1f9016cc060568d7e6f10c Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:31:05 +0100 Subject: [PATCH 29/89] Contributing: Reference Plugin logging documentation --- Contributing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Contributing.md b/Contributing.md index 79ccc8b8..07fd7fae 100644 --- a/Contributing.md +++ b/Contributing.md @@ -319,6 +319,8 @@ except Exception as e: # Try to be more specific from EDMarketConnector import logger ``` +Setting up [logging in plugins](./PLUGINS.md#logging) is slightly different. + We have implemented a `logging.Filter` that adds support for the following in `logging.Formatter()` strings: From f7272ee445cf9b6028dc40d77a008aa8478a5b00 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:32:12 +0100 Subject: [PATCH 30/89] Contributing: fstrings are mandatory, not only preferred Legacy code aside.... --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 07fd7fae..46f39d77 100644 --- a/Contributing.md +++ b/Contributing.md @@ -378,7 +378,7 @@ In addition to that we utilise one of the user-defined levels as: --- -## Prefer fstrings to modulo-formatting and .format +## Use fstrings, not modulo-formatting or .format [fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, and allow for string interpolation rather than more opaque formatting calls. From da790826e99571c20399028d8c6866e54b4d8273 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:33:12 +0100 Subject: [PATCH 31/89] Contributing: missing/incorrect docstrings will fail flake8 --- Contributing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Contributing.md b/Contributing.md index 46f39d77..3cd30877 100644 --- a/Contributing.md +++ b/Contributing.md @@ -393,6 +393,9 @@ literals. Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code. We use the `sphinx` formatting style, which for pycharm users is the default. +Lack of docstrings, or them not passing some checks, *will* cause a flake8 +failure in our setup. + --- ## Comments From 5c8b5b99148114740a9910c8e403a9627db302d0 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:39:18 +0100 Subject: [PATCH 32/89] Translations: Correct example of a non-translated {WORD} The code needs to perform the replacement *after* translation phrase look up. --- docs/Translations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Translations.md b/docs/Translations.md index 89e3f850..ce26c588 100644 --- a/docs/Translations.md +++ b/docs/Translations.md @@ -13,7 +13,7 @@ If you add any new strings that appear in the application UI, e.g. new configura 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')) + _('Some text with a {WORD} not translated').format(WORD='word') This way 'word' will always be used literally. Next you will need to edit `L10n/en.template` to add the phrase: From d304c3260277535bf626a56041b51cbc581ba732 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:47:48 +0100 Subject: [PATCH 33/89] Translations: LANG comment in here, not Contributing Also changed to `# header` style formatting, with `---` separators between major sections. --- Contributing.md | 13 +++---------- docs/Translations.md | 44 ++++++++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Contributing.md b/Contributing.md index 3cd30877..3009a086 100644 --- a/Contributing.md +++ b/Contributing.md @@ -400,17 +400,10 @@ failure in our setup. ## Comments -### Add comments to LANG usage +### LANG comments for translations -Sometimes our translators may need some additional information about what a translation is used for. You can add -that information automatically by using `# LANG: your message here` -**on the line directly above your usage, or at the end of the line in your usage**. If both comments exist, the one -on the current line is preferred over the one above - -```py -# LANG: this says stuff. -_('stuff') -``` +When adding translations you *must* +[add a LANG comment](./docs/Translations.md#add-a-lang-comment). ### Mark hacks and workarounds with a specific comment diff --git a/docs/Translations.md b/docs/Translations.md index ce26c588..6516269d 100644 --- a/docs/Translations.md +++ b/docs/Translations.md @@ -1,11 +1,14 @@ -Introduction -=== +# Translations in Elite Dangerous Market Connector + Translations are handled on [OneSky](https://oneskyapp.com/), specifically in [this project](https://marginal.oneskyapp.com/collaboration/project/52710). -Adding A New Phrase -=== -Setting it up in the code --- + +## Adding A New Phrase + +### Setting it up in the code + +#### Call `_(...)` 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') @@ -16,7 +19,20 @@ If you need to specify something in the text that shouldn't be translated then u _('Some text with a {WORD} not translated').format(WORD='word') This way 'word' will always be used literally. -Next you will need to edit `L10n/en.template` to add the phrase: +#### Add a LANG comment + +Sometimes our translators may need some additional information about what a +translation is used for. You can add that information automatically by using +`# LANG: your message here` **on the line directly above your usage, or at the +end of the line in your usage**. If both comments exist, the one on the +current line is preferred over the one above + +```py +# LANG: this says stuff. +_('stuff') +``` + +#### Edit `L10n/en.template` to add the phrase /* <use of this phrase> [<file it was first added in>] */ "<text as it appears in the code>" = "<English version of the text>"; @@ -43,8 +59,8 @@ You can even use other translations within a given string, e.g.: /* 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."; -Adding it to the OneSky project ---- +## Adding it to the OneSky project + You will, of course, need admin access to the project. Jonathan Harris (aka Maringal, aka Otis) still handles this. Check for this email address in github commits if you need to get in touch. 1. Copy `L10n/en.template` to `en.strings` somewhere. It needs to be this name for OneSky to accept it as an upload. @@ -56,8 +72,10 @@ You will, of course, need admin access to the project. Jonathan Harris (aka Mar All project admins will get a notification of the new upload. Now you wait for translators to work on the new/changed phrases. -Updating Translations In The Code -=== +--- + +## Updating Translations In The Code + Once you have new/changed translations on OneSky you'll want to update the code to use them. 1. Navigate to the [Translation Overview](https://marginal.oneskyapp.com/admin/project/dashboard/project/52710) then click on "Download Translation" which should bring you to [Download](https://marginal.oneskyapp.com/admin/export/phrases/project/52710). @@ -69,8 +87,10 @@ Once you have new/changed translations on OneSky you'll want to update the code 1. Rename the "en.strings" file to "en.template". 1. Commit the changes to git. -Adding a New Language -=== +--- + +## Adding a New Language + To add a new language to the app: 1. Add it to the OneSkyApp project: From 9d46b6a7b06d09adca6c7136a46abd67554e7e4a Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 12:58:45 +0100 Subject: [PATCH 34/89] Contributing: .format() still acceptable wrt translations --- Contributing.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Contributing.md b/Contributing.md index 3009a086..e1086de7 100644 --- a/Contributing.md +++ b/Contributing.md @@ -380,11 +380,16 @@ In addition to that we utilise one of the user-defined levels as: ## Use fstrings, not modulo-formatting or .format -[fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, and allow for string interpolation rather -than more opaque formatting calls. +[fstrings](https://www.python.org/dev/peps/pep-0498/) are new in python 3.6, +and allow for string interpolation rather than more opaque formatting calls. -As part of our flake8 linting setup we have included a linter that warns when you use either `%` or `.format` on string -literals. +As part of our flake8 linting setup we have included a linter that warns when +you use `%` on string literals. + +`.format()` won't throw flake8 errors, **but only because it's still the +best way to handle [untranslated words](./docs/Translations.md#call-_) +in otherwise translated phrases**. Thus, we allow this, and only this, use of +`.format()` for strings. --- From a1abb46ff5d4d17504fc1e1b14f4321a4305d57b Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 13:37:27 +0100 Subject: [PATCH 35/89] l10n script: Fix/add some args help --- scripts/find_localised_strings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index b62ddd78..78f46157 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -270,8 +270,8 @@ if __name__ == '__main__': parser.add_argument('--ignore', action='append', help='directories to ignore', default=['venv', '.git']) group = parser.add_mutually_exclusive_group() group.add_argument('--json', action='store_true', help='JSON output') - group.add_argument('--lang', action='store_true', help='lang file outpot') - group.add_argument('--compare-lang') + group.add_argument('--lang', action='store_true', help='en.template "strings" output') + group.add_argument('--compare-lang', help='en.template file to compare against') args = parser.parse_args() From 1b3046a4f9ddad288f3522ec89d9e6d146d56c57 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 14:07:44 +0100 Subject: [PATCH 36/89] l10n script: Fix comparison for picking up 'before' LANG comments --- scripts/find_localised_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 78f46157..7b38aa1e 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -68,7 +68,7 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op above = call.lineno - 2 current = call.lineno - 1 - above_line = lines[above].strip() if len(lines) < above else None + above_line = lines[above].strip() if len(lines) >= above else None current_line = lines[current].strip() for line in (above_line, current_line): From 87ff0b08a1f6a2f21b58a762a48fc62380f68a72 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 14:17:28 +0100 Subject: [PATCH 37/89] Translations: Full phrase "Error: Check E:D journal file location" --- EDMarketConnector.py | 3 ++- L10n/en.template | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 4fc058ff..be6c59c3 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -694,7 +694,8 @@ class AppWindow(object): # (Re-)install log monitoring if not monitor.start(self.w): - self.status['text'] = f'Error: {_("Check")} {_("E:D journal file location")}' + # LANG: ED Journal file location appears to be in error + self.status['text'] = _('Error: Check E:D journal file location') if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr diff --git a/L10n/en.template b/L10n/en.template index b6afcaae..018742e5 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -589,6 +589,9 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "UI Scale Percentage"; +/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:698; */ +"Error: Check E:D journal file location" = "Error: Check E:D journal file location"; + /* General 'Unknown', e.g. suit loadout */ "Unknown" = "Unknown"; From 6d0f5259d025517287ed9e78822ec75befe041f3 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 14:22:33 +0100 Subject: [PATCH 38/89] Translations: Use script-provided version This loads in to EDMarketConnector.py without errors, and a quick check of German in Settings appeared to show all of the correct translations. --- L10n/en.template | 1183 +++++++++++++++++++++++----------------------- 1 file changed, 584 insertions(+), 599 deletions(-) diff --git a/L10n/en.template b/L10n/en.template index 018742e5..9831bb13 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -1,635 +1,620 @@ -/* Coriolis override modes [plugins/coriolis.py] */ -"Normal" = "Normal"; - -/* Coriolis override modes [plugins/coriolis.py] */ -"Beta" = "Beta"; - -/* Coriolis override modes [plugins/coriolis.py] */ -"Auto" = "Auto"; - -/* Coriolis override label [plugins/coriolis.py] */ -"Override Beta/Normal Selection" = "Override Beta/Normal Selection"; - -/* Coriolis reset buttons [plugins/coriolis.py] */ -"Reset" = "Reset"; - -/* Coriolis Normal URL label [plugins/coriolis.py] */ -"Normal URL" = "Normal URL"; - -/* Coriolis Beta URL label [plugins/coriolis.py] */ -"Beta URL" = "Beta URL"; - -/* Coriolis config title label [plugins/coriolis.py] */ -"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='"; - -/* Coriolis invalid mode warning [plugins/coriolis.py] */ -"Invalid Coriolis override mode!" = "Invalid Coriolis override mode!"; -/* Language name */ -"!Language" = "English"; - -/* Message next to 'UI Transparency' slider [prefs.py] */ -"100 means fully opaque.{CR}Window is updated in real time" = "100 means fully opaque.{CR}Window is updated in real time"; - -/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ -"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!"; - -/* App menu entry on OSX. [EDMarketConnector.py] */ -"About {APP}" = "About {APP}"; - -/* Federation rank. [stats.py] */ -"Admiral" = "Admiral"; - -/* Explorer rank. [stats.py] */ -"Aimless" = "Aimless"; - -/* Appearance setting. [EDMarketConnector.py] */ -"Always on top" = "Always on top"; - -/* CQC rank. [stats.py] */ -"Amateur" = "Amateur"; - -/* EDSM setting. [edsm.py] */ -"API Key" = "API Key"; - -/* Tab heading in settings. [prefs.py] */ -"Appearance" = "Appearance"; - -/* Successfully authenticated with the Frontier website. [EDMarketConnector.py] */ -"Authentication successful" = "Authentication successful"; - -/* Output setting. [prefs.py] */ -"Automatically update on docking" = "Automatically update on docking"; - -/* Cmdr stats. [stats.py] */ -"Balance" = "Balance"; - -/* Empire rank. [stats.py] */ -"Baron" = "Baron"; - -/* Trade rank. [stats.py] */ -"Broker" = "Broker"; - -/* Folder selection button on Windows. [prefs.py] */ -"Browse..." = "Browse..."; - -/* Federation rank. [stats.py] */ -"Cadet" = "Cadet"; - -/* No 'commander' data in CAPI [EDMarketConnector.py] */ -"CAPI: No commander data returned" = "CAPI: No commander data returned"; - -/* CQC rank. [stats.py] */ -"Champion" = "Champion"; - -/* Folder selection button on OSX. [prefs.py] */ -"Change..." = "Change..."; - -/* Generic 'Check', as in 'Check E:D journal file location' [EDMarketConnector.py] */ -"Check" = "Check"; - -/* Menu item. [EDMarketConnector.py] */ -"Check for Updates..." = "Check for Updates..."; - -/* Federation rank. [stats.py] */ -"Chief Petty Officer" = "Chief Petty Officer"; - -/* Main window. [EDMarketConnector.py] */ -"Cmdr" = "Cmdr"; - -/* Ranking. [stats.py] */ -"Combat" = "Combat"; - -/* EDSM setting. [edsm.py] */ -"Commander Name" = "Commander Name"; - -/* Combat rank. [stats.py] */ -"Competent" = "Competent"; - -/* Tab heading in settings. [prefs.py] */ -"Configuration" = "Configuration"; - -/* Update button in main window. [EDMarketConnector.py] */ -"cooldown {SS}s" = "cooldown {SS}s"; - -/* As in Copy and Paste. [EDMarketConnector.py] */ -"Copy" = "Copy"; - -/* Empire rank. [stats.py] */ -"Count" = "Count"; - -/* Ranking. [stats.py] */ -"CQC" = "CQC"; - -/* Combat rank. [stats.py] */ -"Dangerous" = "Dangerous"; - -/* Appearance theme setting. [prefs.py] */ -"Dark" = "Dark"; - -/* Combat rank. [stats.py] */ -"Deadly" = "Deadly"; - -/* Trade rank. [stats.py] */ -"Dealer" = "Dealer"; - -/* Appearance theme and language setting. [l10n.py] */ -"Default" = "Default"; - -/* Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2. [eddn.py] */ -"Delay sending until docked" = "Delay sending until docked"; - -/* Option to disabled Automatic Check For Updates whilst in-game [prefs.py] */ -"Disable Automatic Application Updates Check when in-game" = "Disable Automatic Application Updates Check when in-game"; - -/* List of plugins in settings. [prefs.py] */ -"Disabled Plugins" = "Disabled Plugins"; - -/* Help menu item. [EDMarketConnector.py] */ -"Documentation" = "Documentation"; - -/* Empire rank. [stats.py] */ -"Duke" = "Duke"; - -/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ -"E:D journal file location" = "E:D journal file location"; - -/* When EDDB functionality has been killswitched. [plugins/eddb.py] */ -"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log."; - -/* When EDDN journal handling has been killswitched [plugins/eddn.py] */ -"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log."; - -/* When EDSM functionality has been killswitched [plugins/edsm.py] */ -"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log."; - -/* Empire rank. [stats.py] */ -"Earl" = "Earl"; - -/* Menu title. [EDMarketConnector.py] */ -"Edit" = "Edit"; - -/* Popup title: Warning about plugins without Python 3.x support [EDMarketConnector.py] */ -"EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins Without Python 3.x Support"; - -/* Top rank. [stats.py] */ -"Elite" = "Elite"; - -/* Section heading in settings. [edsm.py] */ -"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials"; - -/* Ranking. [stats.py] */ -"Empire" = "Empire"; - -/* List of plugins in settings. [prefs.py] */ -"Enabled Plugins" = "Enabled Plugins"; - -/* Federation rank. [stats.py] */ -"Ensign" = "Ensign"; - -/* Trade rank. [stats.py] */ -"Entrepreneur" = "Entrepreneur"; - -/* [companion.py] */ -"Error" = "Error"; - -/* [eddn.py] */ -"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN"; - -/* [edsm.py] */ -"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM"; - -/* [inara.py] */ -"Error: Can't connect to Inara" = "Error: Can't connect to Inara"; - -/* [companion.py] */ -"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id"; - -/* [companion.py] */ -"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!"; - -/* [edsm.py] */ -"Error: EDSM {MSG}" = "Error: EDSM {MSG}"; - - -/* Raised when cannot contact the Companion API server. [companion.py] */ +/* In files: companion.py:170; */ "Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond"; -/* OLD: Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier server is down" = "Error: Frontier server is down"; - -/* Raised when Companion API server is returning old data, e.g. when the servers are too busy. [companion.py] */ +/* In files: companion.py:183; */ "Error: Frontier server is lagging" = "Error: Frontier server is lagging"; -/* Raised when the Companion API server thinks that the user has not purchased E:D. i.e. doesn't have the correct 'SKU'. [companion.py] */ +/* In files: companion.py:196; */ "Error: Frontier server SKU problem" = "Error: Frontier server SKU problem"; -/* [inara.py] */ -"Error: Inara {MSG}" = "Error: Inara {MSG}"; - -/* [companion.py] */ +/* In files: companion.py:205; */ "Error: Invalid Credentials" = "Error: Invalid Credentials"; -/* [companion.py] */ -"Error: unable to get token" = "Error: unable to get token"; - -/* Raised when the user has multiple accounts and the username/password setting is not for the account they're currently playing OR the user has reset their Cmdr and the Companion API server is still returning data for the old Cmdr. [companion.py] */ +/* In files: companion.py:220; */ "Error: Wrong Cmdr" = "Error: Wrong Cmdr"; -/* Item in the File menu on Windows. [EDMarketConnector.py] */ -"Exit" = "Exit"; +/* In files: companion.py:330; companion.py:406; */ +"Error" = "Error"; -/* Combat rank. [stats.py] */ -"Expert" = "Expert"; +/* In files: companion.py:368; companion.py:372; */ +"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id"; -/* Ranking. [stats.py] */ -"Explorer" = "Explorer"; +/* In files: companion.py:377; */ +"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!"; -/* Ranking. [stats.py] */ -"Federation" = "Federation"; +/* In files: companion.py:398; */ +"Error: unable to get token" = "Error: unable to get token"; -/* [EDMarketConnector.py] */ -"Fetching data..." = "Fetching data..."; - -/* Multicrew role. [EDMarketConnector.py] */ -"Fighter" = "Fighter"; - -/* Menu title. [EDMarketConnector.py] */ -"File" = "File"; - -/* Section heading in settings. [prefs.py] */ -"File location" = "File location"; - -/* Failures to access Frontier CAPI endpoints [companion.py] */ +/* In files: companion.py:542; */ "Frontier CAPI query failure" = "Frontier CAPI query failure"; -/* Server errors from Frontier CAPI server [companion.py] */ +/* In files: companion.py:557; */ "Frontier CAPI server error" = "Frontier CAPI server error"; -/* CQC rank. [stats.py] */ -"Gladiator" = "Gladiator"; +/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:418; EDMarketConnector.py:711; EDMarketConnector.py:1259; */ +"Update" = "Update"; -/* Multicrew role. [EDMarketConnector.py] */ -"Gunner" = "Gunner"; +/* In files: EDMarketConnector.py:500; prefs.py:788; */ +"Always on top" = "Always on top"; -/* Combat rank. [stats.py] */ -"Harmless" = "Harmless"; - -/* Multicrew role. [EDMarketConnector.py] */ -"Helm" = "Helm"; - -/* Menu title. [EDMarketConnector.py] */ -"Help" = "Help"; - -/* CQC rank. [stats.py] */ -"Helpless" = "Helpless"; - -/* CQC rank. [stats.py] */ -"Hero" = "Hero"; - -/* Dark theme color setting. [prefs.py] */ -"Highlighted text" = "Highlighted text"; - -/* Hotkey/Shortcut settings prompt on Windows. [prefs.py] */ -"Hotkey" = "Hotkey"; - -/* Changed journal update_lock failed [monitor.py] */ -"Ignore" = "Ignore"; - -/* Section heading in settings. [inara.py] */ -"Inara credentials" = "Inara credentials"; - -/* When Inara support has been killswitched [plugins/inara.py] */ -"Inara disabled. See Log." = "Inara disabled. See Log."; - -/* Settings>Plugins>Information on migrating plugins [prefs.py] */ -"Information on migrating plugins" = "Information on migrating plugins"; - -/* Changed journal update_lock failed [monitor.py] */ -"Journal directory already locked" = "Journal directory already locked"; - -/* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */ -"Keyboard shortcut" = "Keyboard shortcut"; - -/* Empire rank. [stats.py] */ -"King" = "King"; - -/* Empire rank. [stats.py] */ -"Knight" = "Knight"; - -/* Appearance setting prompt. [prefs.py] */ -"Language" = "Language"; - -/* [EDMarketConnector.py] - Leave '%H:%M:%S' as-is */ -"Last updated at %H:%M:%S" = "Last updated at %H:%M:%S"; - -/* Federation rank. [stats.py] */ -"Lieutenant" = "Lieutenant"; - -/* Federation rank. [stats.py] */ -"Lieutenant Commander" = "Lieutenant Commander"; - -/* Cmdr stats. [stats.py] */ -"Loan" = "Loan"; - -/* Label for user configured level of logging [prefs.py] */ -"Log Level" = "Log Level"; - -/* [EDMarketConnector.py] */ -"Logging in..." = "Logging in..."; - -/* Empire rank. [stats.py] */ -"Lord" = "Lord"; - -/* Label for 'UI Transparency' option in [prefs.py] */ -"Main window transparency" = "Main window transparency"; - -/* [prefs.py] */ -"Market data in CSV format file" = "Market data in CSV format file"; - -/* [prefs.py] */ -"Market data in Trade Dangerous format file" = "Market data in Trade Dangerous format file"; - -/* Empire rank. [stats.py] */ -"Marquis" = "Marquis"; - -/* Combat rank. [stats.py] */ -"Master" = "Master"; - -/* Trade rank. [stats.py] */ -"Merchant" = "Merchant"; - -/* Federation rank. [stats.py] */ -"Midshipman" = "Midshipman"; - -/* Appearance setting. [EDMarketConnector.py] */ -"Minimize to system tray" = "Minimize to system tray"; - -/* Explorer rank. [stats.py] */ -"Mostly Aimless" = "Mostly Aimless"; - -/* Combat rank. [stats.py] */ -"Mostly Harmless" = "Mostly Harmless"; - -/* CQC rank. [stats.py] */ -"Mostly Helpless" = "Mostly Helpless"; - -/* Trade rank. [stats.py] */ -"Mostly Penniless" = "Mostly Penniless"; - -/* No hotkey/shortcut currently defined. [prefs.py] */ -"None" = "None"; - -/* Dark theme color setting. [prefs.py] */ -"Normal text" = "Normal text"; - -/* Combat rank. [stats.py] */ -"Novice" = "Novice"; - -/* [prefs.py] */ -"OK" = "OK"; - -/* 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."; - -/* Hotkey/Shortcut setting. [prefs.py] */ -"Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app"; - -/* Button that opens a folder in Explorer/Finder. [prefs.py] */ -"Open" = "Open"; - -/* Shortcut settings button on OSX. [prefs.py] */ -"Open System Preferences" = "Open System Preferences"; - -/* Tab heading in settings. [prefs.py] */ -"Output" = "Output"; - -/* Empire rank. [stats.py] */ -"Outsider" = "Outsider"; - -/* Explorer rank. [stats.py] */ -"Pathfinder" = "Pathfinder"; - -/* Trade rank. [stats.py] */ -"Peddler" = "Peddler"; - -/* Trade rank. [stats.py] */ -"Penniless" = "Penniless"; - -/* Federation rank. [stats.py] */ -"Petty Officer" = "Petty Officer"; - -/* Explorer rank. [stats.py] */ -"Pioneer" = "Pioneer"; - -/* Hotkey/Shortcut setting. [prefs.py] */ -"Play sound" = "Play sound"; - -/* [prefs.py] */ -"Please choose what data to save" = "Please choose what data to save"; - -/* Tab heading in settings. [prefs.py] */ -"Plugins" = "Plugins"; - -/* Section heading in settings. [prefs.py] */ -"Plugins folder" = "Plugins folder"; - -/* Settings>Plugins>Plugins without Python 3.x support [prefs.py] */ -"Plugins Without Python 3.x Support:" = "Plugins Without Python 3.x Support:"; - -/* Federation rank. [stats.py] */ -"Post Captain" = "Post Captain"; - -/* Federation rank. [stats.py] */ -"Post Commander" = "Post Commander"; - -/* Ranking. [stats.py] */ -"Powerplay" = "Powerplay"; - -/* [prefs.py] */ -"Preferences" = "Preferences"; - -/* Settings prompt for preferred ship loadout, system and station info websites. [prefs.py] */ -"Preferred websites" = "Preferred websites"; - -/* Empire rank. [stats.py] */ -"Prince" = "Prince"; - -/* Help menu item. [EDMarketConnector.py] */ -"Privacy Policy" = "Privacy Policy"; - -/* CQC rank. [stats.py] */ -"Professional" = "Professional"; - -/* Explorer rank. [stats.py] */ -"Ranger" = "Ranger"; - -/* Power rank. [stats.py] */ -"Rating 1" = "Rating 1"; - -/* Power rank. [stats.py] */ -"Rating 2" = "Rating 2"; - -/* Power rank. [stats.py] */ -"Rating 3" = "Rating 3"; - -/* Power rank. [stats.py] */ -"Rating 4" = "Rating 4"; - -/* Power rank. [stats.py] */ -"Rating 5" = "Rating 5"; - -/* Shortcut settings prompt on OSX. [prefs.py] */ -"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts"; - -/* Federation rank. [stats.py] */ -"Rear Admiral" = "Rear Admiral"; - -/* Federation rank. [stats.py] */ -"Recruit" = "Recruit"; - -/* Help menu item. [EDMarketConnector.py] */ -"Release Notes" = "Release Notes"; - -/* Changed journal update_lock failed [monitor.py] */ -"Retry" = "Retry"; - -/* Multicrew role label in main window. [EDMarketConnector.py] */ -"Role" = "Role"; - -/* Menu item. [EDMarketConnector.py] */ -"Save Raw Data..." = "Save Raw Data..."; - -/* Explorer rank. [stats.py] */ -"Scout" = "Scout"; - -/* CQC rank. [stats.py] */ -"Semi Professional" = "Semi Professional"; - -/* [edsm.py] */ -"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM"; - -/* [inara.py] */ -"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara"; - -/* Output setting. [eddn.py] */ -"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network"; - -/* Output setting new in E:D 2.2. [eddn.py] */ -"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network"; - -/* [eddn.py] */ -"Sending data to EDDN..." = "Sending data to EDDN..."; - -/* Empire rank. [stats.py] */ -"Serf" = "Serf"; - -/* Item in the File menu on Windows. [EDMarketConnector.py] */ -"Settings" = "Settings"; - -/* Main window. [EDMarketConnector.py] */ -"Ship" = "Ship"; - -/* Output setting. [prefs.py] */ -"Ship loadout" = "Ship loadout"; - -/* Status dialog title. [stats.py] */ -"Ships" = "Ships"; - -/* Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis. [prefs.py] */ -"Shipyard" = "Shipyard"; - -/* Status line text that appears when process exit sequence starts [EDMarketConnector.py] */ -"Shutting down..." = "Shutting down..."; - -/* Empire rank. [stats.py] */ -"Squire" = "Squire"; - -/* Main window. [EDMarketConnector.py] */ -"Station" = "Station"; - -/* [EDMarketConnector.py] */ -"Station doesn't have a market!" = "Station doesn't have a market!"; - -/* [EDMarketConnector.py] */ -"Station doesn't have anything!" = "Station doesn't have anything!"; - -/* Menu item. [EDMarketConnector.py] */ -"Status" = "Status"; - -/* 'Suit' label in main UI' [EDMarketConnector.py] */ -"Suit" = "Suit"; - -/* Explorer rank. [stats.py] */ -"Surveyor" = "Surveyor"; - -/* Main window. [EDMarketConnector.py] */ -"System" = "System"; - -/* Changed journal update_lock failed [monitor.py] */ -"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this."; - -/* Appearance setting. [prefs.py] */ -"Theme" = "Theme"; - -/* Help text in settings. [prefs.py] */ -"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"; - -/* Ranking. [stats.py] */ -"Trade" = "Trade"; - -/* Explorer rank. [stats.py] */ -"Trailblazer" = "Trailblazer"; - -/* Appearance theme setting. [prefs.py] */ -"Transparent" = "Transparent"; - -/* Trade rank. [stats.py] */ -"Tycoon" = "Tycoon"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scale Percentage" = "UI Scale Percentage"; +/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:629; */ +"Unknown" = "Unknown"; /* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:698; */ "Error: Check E:D journal file location" = "Error: Check E:D journal file location"; -/* General 'Unknown', e.g. suit loadout */ -"Unknown" = "Unknown"; +/* EDMarketConnector.py: Main window; stats.py: Cmdr stats; In files: EDMarketConnector.py:705; edsm.py:216; stats.py:50; theme.py:226; */ +"Cmdr" = "Cmdr"; -/* Update button in main window. [EDMarketConnector.py] */ -"Update" = "Update"; +/* EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:707; EDMarketConnector.py:1029; */ +"Role" = "Role"; -/* Option to use alternate URL method on shipyard links [prefs.py] */ -"Use alternate URL method" = "Use alternate URL method"; +/* EDMarketConnector.py: Main window; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:707; EDMarketConnector.py:1039; EDMarketConnector.py:1062; stats.py:362; */ +"Ship" = "Ship"; -/* Status dialog subtitle - CR value of ship. [stats.py] */ -"Value" = "Value"; +/* EDMarketConnector.py: Main window; In files: EDMarketConnector.py:708; */ +"Suit" = "Suit"; -/* Federation rank. [stats.py] */ -"Vice Admiral" = "Vice Admiral"; +/* EDMarketConnector.py: Main window; stats.py: Main window; In files: EDMarketConnector.py:709; prefs.py:567; stats.py:364; */ +"System" = "System"; -/* Menu title on OSX. [EDMarketConnector.py] */ +/* EDMarketConnector.py: Main window; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:710; prefs.py:584; prefs.py:688; stats.py:365; */ +"Station" = "Station"; + +/* EDMarketConnector.py: Menu title on OSX; EDMarketConnector.py: Menu title; EDMarketConnector.py: words for use in python 2 plugin error; In files: EDMarketConnector.py:713; EDMarketConnector.py:726; EDMarketConnector.py:729; EDMarketConnector.py:1736; */ +"File" = "File"; + +/* EDMarketConnector.py: Menu title on OSX; EDMarketConnector.py: Menu title; In files: EDMarketConnector.py:714; EDMarketConnector.py:727; EDMarketConnector.py:730; */ +"Edit" = "Edit"; + +/* EDMarketConnector.py: Menu title on OSX; In files: EDMarketConnector.py:715; */ "View" = "View"; -/* Empire rank. [stats.py] */ -"Viscount" = "Viscount"; - -/* Federation rank. [stats.py] */ -"Warrant Officer" = "Warrant Officer"; - -/* Shouldn't happen. [EDMarketConnector.py] */ -"What are you flying?!" = "What are you flying?!"; - -/* Shouldn't happen. [EDMarketConnector.py] */ -"Where are you?!" = "Where are you?!"; - -/* Shouldn't happen. [EDMarketConnector.py] */ -"Who are you?!" = "Who are you?!"; - -/* Menu title on OSX. [EDMarketConnector.py] */ +/* EDMarketConnector.py: Menu title on OSX; In files: EDMarketConnector.py:716; */ "Window" = "Window"; -/* [EDMarketConnector.py] */ +/* EDMarketConnector.py: Menu title on OSX; EDMarketConnector.py: Menu title; In files: EDMarketConnector.py:717; EDMarketConnector.py:728; EDMarketConnector.py:731; */ +"Help" = "Help"; + +/* EDMarketConnector.py: Menu title on OSX; EDMarketConnector.py: App menu entry; In files: EDMarketConnector.py:718; EDMarketConnector.py:744; EDMarketConnector.py:1303; */ +"About {APP}" = "About {APP}"; + +/* EDMarketConnector.py: Menu item; In files: EDMarketConnector.py:720; EDMarketConnector.py:743; */ +"Check for Updates..." = "Check for Updates..."; + +/* EDMarketConnector.py: Menu item; In files: EDMarketConnector.py:721; EDMarketConnector.py:735; */ +"Save Raw Data..." = "Save Raw Data..."; + +/* EDMarketConnector.py: Menu item; stats.py: Status dialog title; In files: EDMarketConnector.py:722; EDMarketConnector.py:734; stats.py:359; */ +"Status" = "Status"; + +/* EDMarketConnector.py: Help menu item; In files: EDMarketConnector.py:723; EDMarketConnector.py:741; */ +"Privacy Policy" = "Privacy Policy"; + +/* EDMarketConnector.py: Help menu item; In files: EDMarketConnector.py:724; EDMarketConnector.py:742; EDMarketConnector.py:1336; */ +"Release Notes" = "Release Notes"; + +/* EDMarketConnector.py: Item in the File menu on Windows; EDMarketConnector.py: words for use in python 2 plugin error; In files: EDMarketConnector.py:736; EDMarketConnector.py:1736; prefs.py:248; */ +"Settings" = "Settings"; + +/* EDMarketConnector.py: Item in the File menu on Windows; In files: EDMarketConnector.py:737; */ +"Exit" = "Exit"; + +/* EDMarketConnector.py: Help menu item; In files: EDMarketConnector.py:740; */ +"Documentation" = "Documentation"; + +/* EDMarketConnector.py: As in Copy and Paste; In files: EDMarketConnector.py:747; ttkHyperlinkLabel.py:41; */ +"Copy" = "Copy"; + +/* In files: EDMarketConnector.py:752; */ +"Logging in..." = "Logging in..."; + +/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:768; EDMarketConnector.py:1172; */ +"Authentication successful" = "Authentication successful"; + +/* In files: EDMarketConnector.py:798; */ "You're not docked at a station!" = "You're not docked at a station!"; -/* Shortcut settings prompt on OSX. [prefs.py] */ +/* In files: EDMarketConnector.py:805; */ +"Station doesn't have anything!" = "Station doesn't have anything!"; + +/* In files: EDMarketConnector.py:809; */ +"Station doesn't have a market!" = "Station doesn't have a market!"; + +/* In files: EDMarketConnector.py:853; EDMarketConnector.py:1381; stats.py:278; */ +"Fetching data..." = "Fetching data..."; + +/* In files: EDMarketConnector.py:865; */ +"CAPI: No commander data returned" = "CAPI: No commander data returned"; + +/* stats.py: Unknown commander; In files: EDMarketConnector.py:868; stats.py:296; */ +"Who are you?!" = "Who are you?!"; + +/* stats.py: Unknown location; In files: EDMarketConnector.py:873; stats.py:306; */ +"Where are you?!" = "Where are you?!"; + +/* stats.py: Unknown ship; In files: EDMarketConnector.py:876; stats.py:311; */ +"What are you flying?!" = "What are you flying?!"; + +/* In files: EDMarketConnector.py:983; */ +"Last updated at %H:%M:%S" = "Last updated at %H:%M:%S"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1009; */ +"Fighter" = "Fighter"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1010; */ +"Gunner" = "Gunner"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1011; */ +"Helm" = "Helm"; + +/* In files: EDMarketConnector.py:1255; */ +"cooldown {SS}s" = "cooldown {SS}s"; + +/* In files: EDMarketConnector.py:1361; prefs.py:297; */ +"OK" = "OK"; + +/* In files: EDMarketConnector.py:1441; */ +"Shutting down..." = "Shutting down..."; + +/* In files: EDMarketConnector.py:1726:1732; */ +"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."; + +/* EDMarketConnector.py: words for use in python 2 plugin error; In files: EDMarketConnector.py:1736; prefs.py:888; */ +"Plugins" = "Plugins"; + +/* In files: EDMarketConnector.py:1743; */ +"EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins Without Python 3.x Support"; + +/* In files: journal_lock.py:205; */ +"Journal directory already locked" = "Journal directory already locked"; + +/* In files: journal_lock.py:221:222; */ +"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this."; + +/* In files: journal_lock.py:225; */ +"Retry" = "Retry"; + +/* In files: journal_lock.py:228; */ +"Ignore" = "Ignore"; + +/* In files: l10n.py:193; prefs.py:438; prefs.py:636; prefs.py:664; */ +"Default" = "Default"; + +/* In files: coriolis.py:51; coriolis.py:51; coriolis.py:88; coriolis.py:104; coriolis.py:110; */ +"Auto" = "Auto"; + +/* In files: coriolis.py:51; coriolis.py:88; coriolis.py:102; */ +"Normal" = "Normal"; + +/* In files: coriolis.py:51; coriolis.py:88; coriolis.py:103; */ +"Beta" = "Beta"; + +/* In files: coriolis.py:64:66; */ +"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='"; + +/* In files: coriolis.py:69; */ +"Normal URL" = "Normal URL"; + +/* In files: coriolis.py:71; coriolis.py:78; */ +"Reset" = "Reset"; + +/* In files: coriolis.py:76; */ +"Beta URL" = "Beta URL"; + +/* In files: coriolis.py:83; */ +"Override Beta/Normal Selection" = "Override Beta/Normal Selection"; + +/* In files: coriolis.py:120; */ +"Invalid Coriolis override mode!" = "Invalid Coriolis override mode!"; + +/* In files: eddb.py:99; */ +"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log."; + +/* In files: eddn.py:215; eddn.py:593; eddn.py:940; */ +"Sending data to EDDN..." = "Sending data to EDDN..."; + +/* In files: eddn.py:260; eddn.py:878; eddn.py:913; eddn.py:952; */ +"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN"; + +/* In files: eddn.py:672; */ +"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network"; + +/* In files: eddn.py:682; */ +"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network"; + +/* In files: eddn.py:692; */ +"Delay sending until docked" = "Delay sending until docked"; + +/* In files: eddn.py:756; */ +"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log."; + +/* In files: edsm.py:197; */ +"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM"; + +/* In files: edsm.py:206; */ +"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials"; + +/* In files: edsm.py:223; */ +"Commander Name" = "Commander Name"; + +/* In files: edsm.py:230; inara.py:233; */ +"API Key" = "API Key"; + +/* stats.py: No rank; In files: edsm.py:256; prefs.py:487; prefs.py:1091; prefs.py:1123; stats.py:117; stats.py:136; stats.py:155; stats.py:172; */ +"None" = "None"; + +/* In files: edsm.py:351; */ +"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log."; + +/* In files: edsm.py:632; edsm.py:734; */ +"Error: EDSM {MSG}" = "Error: EDSM {MSG}"; + +/* In files: edsm.py:668; edsm.py:730; */ +"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM"; + +/* In files: inara.py:215; */ +"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara"; + +/* In files: inara.py:225; */ +"Inara credentials" = "Inara credentials"; + +/* In files: inara.py:331; */ +"Inara disabled. See Log." = "Inara disabled. See Log."; + +/* In files: inara.py:1537; inara.py:1549; */ +"Error: Inara {MSG}" = "Error: Inara {MSG}"; + +/* In files: prefs.py:248; */ +"Preferences" = "Preferences"; + +/* In files: prefs.py:338; */ +"Please choose what data to save" = "Please choose what data to save"; + +/* In files: prefs.py:344; */ +"Market data in CSV format file" = "Market data in CSV format file"; + +/* In files: prefs.py:353; */ +"Market data in Trade Dangerous format file" = "Market data in Trade Dangerous format file"; + +/* In files: prefs.py:363; */ +"Ship loadout" = "Ship loadout"; + +/* In files: prefs.py:373; */ +"Automatically update on docking" = "Automatically update on docking"; + +/* In files: prefs.py:381; prefs.py:391; */ +"File location" = "File location"; + +/* In files: prefs.py:390; prefs.py:429; */ +"Change..." = "Change..."; + +/* In files: prefs.py:390; prefs.py:429; */ +"Browse..." = "Browse..."; + +/* In files: prefs.py:397; */ +"Output" = "Output"; + +/* In files: prefs.py:422; prefs.py:430; */ +"E:D journal file location" = "E:D journal file location"; + +/* In files: prefs.py:454; */ +"Keyboard shortcut" = "Keyboard shortcut"; + +/* In files: prefs.py:456; */ +"Hotkey" = "Hotkey"; + +/* In files: prefs.py:464; */ +"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts"; + +/* In files: prefs.py:472; */ "{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts"; + +/* In files: prefs.py:477; */ +"Open System Preferences" = "Open System Preferences"; + +/* In files: prefs.py:497; */ +"Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app"; + +/* In files: prefs.py:507; */ +"Play sound" = "Play sound"; + +/* In files: prefs.py:521; */ +"Disable Automatic Application Updates Check when in-game" = "Disable Automatic Application Updates Check when in-game"; + +/* In files: prefs.py:533; */ +"Preferred websites" = "Preferred websites"; + +/* In files: prefs.py:543; */ +"Shipyard" = "Shipyard"; + +/* In files: prefs.py:554; */ +"Use alternate URL method" = "Use alternate URL method"; + +/* In files: prefs.py:604; */ +"Log Level" = "Log Level"; + +/* In files: prefs.py:631; */ +"Configuration" = "Configuration"; + +/* In files: prefs.py:642; */ +"Normal text" = "Normal text"; + +/* In files: prefs.py:643; */ +"Highlighted text" = "Highlighted text"; + +/* In files: prefs.py:651; */ +"Language" = "Language"; + +/* In files: prefs.py:660; */ +"Theme" = "Theme"; + +/* In files: prefs.py:669; */ +"Dark" = "Dark"; + +/* In files: prefs.py:675; */ +"Transparent" = "Transparent"; + +/* In files: prefs.py:719; */ +"UI Scale Percentage" = "UI Scale Percentage"; + +/* In files: prefs.py:739; */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!"; + +/* In files: prefs.py:748; */ +"Main window transparency" = "Main window transparency"; + +/* In files: prefs.py:767:770; */ +"100 means fully opaque.{CR}Window is updated in real time" = "100 means fully opaque.{CR}Window is updated in real time"; + +/* In files: prefs.py:804; */ +"Appearance" = "Appearance"; + +/* In files: prefs.py:815; */ +"Plugins folder" = "Plugins folder"; + +/* In files: prefs.py:823; */ +"Open" = "Open"; + +/* In files: prefs.py:830; */ +"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"; + +/* In files: prefs.py:840; */ +"Enabled Plugins" = "Enabled Plugins"; + +/* In files: prefs.py:859; */ +"Plugins Without Python 3.x Support:" = "Plugins Without Python 3.x Support:"; + +/* In files: prefs.py:866; */ +"Information on migrating plugins" = "Information on migrating plugins"; + +/* In files: prefs.py:880; */ +"Disabled Plugins" = "Disabled Plugins"; + +/* stats.py: Cmdr stats; In files: stats.py:51; */ +"Balance" = "Balance"; + +/* stats.py: Cmdr stats; In files: stats.py:52; */ +"Loan" = "Loan"; + +/* stats.py: Ranking; In files: stats.py:57; */ +"Combat" = "Combat"; + +/* stats.py: Ranking; In files: stats.py:58; */ +"Trade" = "Trade"; + +/* stats.py: Ranking; In files: stats.py:59; */ +"Explorer" = "Explorer"; + +/* stats.py: Ranking; In files: stats.py:60; */ +"CQC" = "CQC"; + +/* stats.py: Ranking; In files: stats.py:61; */ +"Federation" = "Federation"; + +/* stats.py: Ranking; In files: stats.py:62; */ +"Empire" = "Empire"; + +/* stats.py: Ranking; In files: stats.py:63; */ +"Powerplay" = "Powerplay"; + +/* stats.py: Combat rank; In files: stats.py:71; */ +"Harmless" = "Harmless"; + +/* stats.py: Combat rank; In files: stats.py:72; */ +"Mostly Harmless" = "Mostly Harmless"; + +/* stats.py: Combat rank; In files: stats.py:73; */ +"Novice" = "Novice"; + +/* stats.py: Combat rank; In files: stats.py:74; */ +"Competent" = "Competent"; + +/* stats.py: Combat rank; In files: stats.py:75; */ +"Expert" = "Expert"; + +/* stats.py: Combat rank; stats.py: Empire rank; In files: stats.py:76; stats.py:139; */ +"Master" = "Master"; + +/* stats.py: Combat rank; In files: stats.py:77; */ +"Dangerous" = "Dangerous"; + +/* stats.py: Combat rank; In files: stats.py:78; */ +"Deadly" = "Deadly"; + +/* stats.py: Top rank; In files: stats.py:79; stats.py:90; stats.py:101; stats.py:112; */ +"Elite" = "Elite"; + +/* stats.py: Trade rank; In files: stats.py:82; */ +"Penniless" = "Penniless"; + +/* stats.py: Trade rank; In files: stats.py:83; */ +"Mostly Penniless" = "Mostly Penniless"; + +/* stats.py: Trade rank; In files: stats.py:84; */ +"Peddler" = "Peddler"; + +/* stats.py: Trade rank; In files: stats.py:85; */ +"Dealer" = "Dealer"; + +/* stats.py: Trade rank; In files: stats.py:86; */ +"Merchant" = "Merchant"; + +/* stats.py: Trade rank; In files: stats.py:87; */ +"Broker" = "Broker"; + +/* stats.py: Trade rank; In files: stats.py:88; */ +"Entrepreneur" = "Entrepreneur"; + +/* stats.py: Trade rank; In files: stats.py:89; */ +"Tycoon" = "Tycoon"; + +/* stats.py: Explorer rank; In files: stats.py:93; */ +"Aimless" = "Aimless"; + +/* stats.py: Explorer rank; In files: stats.py:94; */ +"Mostly Aimless" = "Mostly Aimless"; + +/* stats.py: Explorer rank; In files: stats.py:95; */ +"Scout" = "Scout"; + +/* stats.py: Explorer rank; In files: stats.py:96; */ +"Surveyor" = "Surveyor"; + +/* stats.py: Explorer rank; In files: stats.py:97; */ +"Trailblazer" = "Trailblazer"; + +/* stats.py: Explorer rank; In files: stats.py:98; */ +"Pathfinder" = "Pathfinder"; + +/* stats.py: Explorer rank; In files: stats.py:99; */ +"Ranger" = "Ranger"; + +/* stats.py: Explorer rank; In files: stats.py:100; */ +"Pioneer" = "Pioneer"; + +/* stats.py: CQC rank; In files: stats.py:104; */ +"Helpless" = "Helpless"; + +/* stats.py: CQC rank; In files: stats.py:105; */ +"Mostly Helpless" = "Mostly Helpless"; + +/* stats.py: CQC rank; In files: stats.py:106; */ +"Amateur" = "Amateur"; + +/* stats.py: CQC rank; In files: stats.py:107; */ +"Semi Professional" = "Semi Professional"; + +/* stats.py: CQC rank; In files: stats.py:108; */ +"Professional" = "Professional"; + +/* stats.py: CQC rank; In files: stats.py:109; */ +"Champion" = "Champion"; + +/* stats.py: CQC rank; In files: stats.py:110; */ +"Hero" = "Hero"; + +/* stats.py: CQC rank; In files: stats.py:111; */ +"Gladiator" = "Gladiator"; + +/* stats.py: Federation rank; In files: stats.py:118; */ +"Recruit" = "Recruit"; + +/* stats.py: Federation rank; In files: stats.py:119; */ +"Cadet" = "Cadet"; + +/* stats.py: Federation rank; In files: stats.py:120; */ +"Midshipman" = "Midshipman"; + +/* stats.py: Federation rank; In files: stats.py:121; */ +"Petty Officer" = "Petty Officer"; + +/* stats.py: Federation rank; In files: stats.py:122; */ +"Chief Petty Officer" = "Chief Petty Officer"; + +/* stats.py: Federation rank; In files: stats.py:123; */ +"Warrant Officer" = "Warrant Officer"; + +/* stats.py: Federation rank; In files: stats.py:124; */ +"Ensign" = "Ensign"; + +/* stats.py: Federation rank; In files: stats.py:125; */ +"Lieutenant" = "Lieutenant"; + +/* stats.py: Federation rank; In files: stats.py:126; */ +"Lieutenant Commander" = "Lieutenant Commander"; + +/* stats.py: Federation rank; In files: stats.py:127; */ +"Post Commander" = "Post Commander"; + +/* stats.py: Federation rank; In files: stats.py:128; */ +"Post Captain" = "Post Captain"; + +/* stats.py: Federation rank; In files: stats.py:129; */ +"Rear Admiral" = "Rear Admiral"; + +/* stats.py: Federation rank; In files: stats.py:130; */ +"Vice Admiral" = "Vice Admiral"; + +/* stats.py: Federation rank; In files: stats.py:131; */ +"Admiral" = "Admiral"; + +/* stats.py: Empire rank; In files: stats.py:137; */ +"Outsider" = "Outsider"; + +/* stats.py: Empire rank; In files: stats.py:138; */ +"Serf" = "Serf"; + +/* stats.py: Empire rank; In files: stats.py:140; */ +"Squire" = "Squire"; + +/* stats.py: Empire rank; In files: stats.py:141; */ +"Knight" = "Knight"; + +/* stats.py: Empire rank; In files: stats.py:142; */ +"Lord" = "Lord"; + +/* stats.py: Empire rank; In files: stats.py:143; */ +"Baron" = "Baron"; + +/* stats.py: Empire rank; In files: stats.py:144; */ +"Viscount" = "Viscount"; + +/* stats.py: Empire rank; In files: stats.py:145; */ +"Count" = "Count"; + +/* stats.py: Empire rank; In files: stats.py:146; */ +"Earl" = "Earl"; + +/* stats.py: Empire rank; In files: stats.py:147; */ +"Marquis" = "Marquis"; + +/* stats.py: Empire rank; In files: stats.py:148; */ +"Duke" = "Duke"; + +/* stats.py: Empire rank; In files: stats.py:149; */ +"Prince" = "Prince"; + +/* stats.py: Empire rank; In files: stats.py:150; */ +"King" = "King"; + +/* stats.py: Power rank; In files: stats.py:156; */ +"Rating 1" = "Rating 1"; + +/* stats.py: Power rank; In files: stats.py:157; */ +"Rating 2" = "Rating 2"; + +/* stats.py: Power rank; In files: stats.py:158; */ +"Rating 3" = "Rating 3"; + +/* stats.py: Power rank; In files: stats.py:159; */ +"Rating 4" = "Rating 4"; + +/* stats.py: Power rank; In files: stats.py:160; */ +"Rating 5" = "Rating 5"; + +/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:366; */ +"Value" = "Value"; + +/* stats.py: Status dialog title; In files: stats.py:375; */ +"Ships" = "Ships"; From 82fd3cd57782c3816c08dcdd2ed386efbd4aa095 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 14:34:12 +0100 Subject: [PATCH 39/89] l10n script: With --lang output the !Language header --- scripts/find_localised_strings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 7b38aa1e..fe33327d 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -239,7 +239,10 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) deduped = dedupe_lang_entries(entries) - out = '' + out = ''' +/* Language name */ +"!Language" = "English"; +''' print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) for entry in deduped: assert len(entry.comments) == len(entry.locations) From be46c47c1c98082eeb1ed54bc89634a672536908 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 14:50:18 +0100 Subject: [PATCH 40/89] l10n script: Adjust !Language header --- scripts/find_localised_strings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index fe33327d..8d19bb68 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -239,9 +239,9 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) deduped = dedupe_lang_entries(entries) - out = ''' -/* Language name */ + out = '''/* Language name */ "!Language" = "English"; + ''' print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) for entry in deduped: From 32101bdba557bf00570debc93fc7701d7fc22e36 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:03:46 +0100 Subject: [PATCH 41/89] Translations: auto-generated file now has !Language header --- L10n/en.template | 3 +++ 1 file changed, 3 insertions(+) diff --git a/L10n/en.template b/L10n/en.template index 9831bb13..3be9753f 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -1,3 +1,6 @@ +/* Language name */ +"!Language" = "English"; + /* In files: companion.py:170; */ "Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond"; From 06725467f9c30bfcbf1eab5296832eafcae0b52d Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:05:26 +0100 Subject: [PATCH 42/89] l10n script: --lang takes a filename argument now This allows for forcing unix-style line endings to the file, rather than needing to mess with dos2unix after on a Windows system. --- scripts/find_localised_strings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 8d19bb68..c65e383e 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -273,7 +273,7 @@ if __name__ == '__main__': parser.add_argument('--ignore', action='append', help='directories to ignore', default=['venv', '.git']) group = parser.add_mutually_exclusive_group() group.add_argument('--json', action='store_true', help='JSON output') - group.add_argument('--lang', action='store_true', help='en.template "strings" output') + group.add_argument('--lang', help='en.template "strings" output to specified file, "-" for stdout') group.add_argument('--compare-lang', help='en.template file to compare against') args = parser.parse_args() @@ -313,7 +313,12 @@ if __name__ == '__main__': print(json.dumps(to_print_data, indent=2)) elif args.lang: - print(generate_lang_template(res)) + if args.lang == '-': + print(generate_lang_template(res)) + + else: + with open(args.lang, mode='w+', newline='\n') as langfile: + langfile.writelines(generate_lang_template(res)) else: for path, calls in res.items(): From 1b58e91fa2704cb81104dec051dfc88b035f481e Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:27:09 +0100 Subject: [PATCH 43/89] l10n script: Only report 'Unknown comment' if we didn't find a LANG one So if we have a before LANG comment, then don't report the actual line for a bad comment. And if we have a current_line valid LANG comment, then don't report the above_line as unknown. --- scripts/find_localised_strings.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index c65e383e..3f1e347c 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -64,38 +64,36 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op :param file: The path to the file this call node came from :return: The first comment that matches the rules, or None """ - out: list[Optional[str]] = [] + out: Optional[str] = None above = call.lineno - 2 current = call.lineno - 1 above_line = lines[above].strip() if len(lines) >= above else None current_line = lines[current].strip() + bad_comment: Optional[str] = None for line in (above_line, current_line): if line is None or '#' not in line: - out.append(None) continue match = COMMENT_RE.match(line) if not match: print(line) - out.append(None) continue comment = match.group(1).strip() if not comment.startswith('# LANG:'): - print(f'Unknown comment for {file}:{current} {line}', file=sys.stderr) - out.append(None) + bad_comment = f'Unknown comment for {file}:{current} {line}' continue - out.append(comment.replace('# LANG:', '').strip()) + out = comment.replace('# LANG:', '').strip() + bad_comment = None + break - if out[1] is not None: - return out[1] - elif out[0] is not None: - return out[0] + if bad_comment is not None: + print(bad_comment, file=sys.stderr) - return None + return out def scan_file(path: pathlib.Path) -> list[ast.Call]: From 2351ee0f0241d1e614982aa0cd90e118dd56dd9b Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:32:52 +0100 Subject: [PATCH 44/89] EDMarketConnector: LANG comment pass --- EDMarketConnector.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index be6c59c3..b3e48b00 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -703,7 +703,7 @@ class AppWindow(object): def set_labels(self): """Set main window labels, e.g. after language change.""" self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Main window - # Multicrew role label 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: Main window self.system_label['text'] = _('System') + ':' # LANG: Main window @@ -764,7 +764,7 @@ class AppWindow(object): self.w.update_idletasks() try: if companion.session.login(monitor.cmdr, monitor.is_beta): - # Successfully authenticated with the Frontier website + # LANG: Successfully authenticated with the Frontier website self.status['text'] = _('Authentication successful') if platform == 'darwin': @@ -795,6 +795,7 @@ class AppWindow(object): if not self.status['text']: # 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.status['text'] = _("You're not docked at a station!") return False @@ -862,17 +863,21 @@ class AppWindow(object): # Validation if 'commander' not in 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') elif not 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 elif (not data.get('lastSystem', {}).get('name') or (data['commander'].get('docked') and not 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 elif not data.get('ship', {}).get('name') or not 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 elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: @@ -980,6 +985,7 @@ class AppWindow(object): play_bad = True 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(querytime)) if play_sound and play_bad: @@ -1438,6 +1444,7 @@ class AppWindow(object): config.set('geometry', f'+{x}+{y}') # Let the user know we're shutting down. + # LANG: The application is shutting down self.status['text'] = _('Shutting down...') self.w.update_idletasks() logger.info('Starting shutdown procedures...') From cc0e39b7c20eb15223e74ec3f2122901f79323dd Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:34:14 +0100 Subject: [PATCH 45/89] l10n.py: LANG comment pass --- l10n.py | 1 + 1 file changed, 1 insertion(+) diff --git a/l10n.py b/l10n.py index d6e4677e..7ffed54b 100755 --- a/l10n.py +++ b/l10n.py @@ -190,6 +190,7 @@ class _Translations: def available_names(self) -> Dict[Optional[str], str]: """Available language names by code.""" names: Dict[Optional[str], str] = OrderedDict([ + # LANG: The system default language choice in Settings > Appearance (None, _('Default')), # Appearance theme and language setting ]) names.update(sorted( From a412ec13820b4eff067eebb48dc78b56d29c1d60 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:35:24 +0100 Subject: [PATCH 46/89] plugins/coriolis.py: LANG comment pass --- plugins/coriolis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/coriolis.py b/plugins/coriolis.py index c276eff4..e28ce0d8 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -99,9 +99,9 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: beta_url = beta_textvar.get() override_mode = override_textvar.get() override_mode = { # Convert to unlocalised names - _('Normal'): 'normal', - _('Beta'): 'beta', - _('Auto'): 'auto', + _('Normal'): 'normal', # LANG: Coriolis normal/beta selection - normal + _('Beta'): 'beta', # LANG: Coriolis normal/beta selection - beta + _('Auto'): 'auto', # LANG: Coriolis normal/beta selection - auto }.get(override_mode, override_mode) if override_mode not in ('beta', 'normal', 'auto'): From 4fa0521438e16f09749087aad379048ca5f6c30c Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:43:32 +0100 Subject: [PATCH 47/89] other core plugins: LANG comment pass --- plugins/eddn.py | 2 +- plugins/edsm.py | 6 +++++- plugins/inara.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index feeb0e5c..693f01fc 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -590,7 +590,7 @@ Msg:\n{msg}''' else: # Can't access replay file! Send immediately. - self.parent.children['status']['text'] = _('Sending data to EDDN...') + self.parent.children['status']['text'] = _('Sending data to EDDN...') # LANG: Data is being sent to EDDN self.parent.update_idletasks() self.send(cmdr, msg) self.parent.children['status']['text'] = '' diff --git a/plugins/edsm.py b/plugins/edsm.py index 9519d83f..458b1740 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -213,6 +213,7 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) + # LANG: Game Commander name label in EDSM settings this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window this.cmdr_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.cmdr_text = nb.Label(frame) @@ -220,6 +221,7 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: cur_row += 1 + # LANG: EDSM Commander name label in EDSM settings this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting this.user_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.user = nb.Entry(frame) @@ -227,6 +229,7 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: cur_row += 1 + # LANG: EDSM API key label this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting this.apikey_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.apikey = nb.Entry(frame) @@ -253,7 +256,8 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: this.apikey.insert(0, cred[1]) else: - this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined + # LANG: We have no data on the current commander + this.cmdr_text['text'] = _('None') to_set = tk.DISABLED if cmdr and not is_beta and this.log.get(): diff --git a/plugins/inara.py b/plugins/inara.py index 5e216007..f4db0098 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -230,6 +230,7 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: this.label.grid(columnspan=2, padx=x_padding, sticky=tk.W) + # LANG: Inara API key label this.apikey_label = nb.Label(frame, text=_('API Key')) # Inara setting this.apikey_label.grid(row=12, padx=x_padding, sticky=tk.W) this.apikey = nb.Entry(frame) From fc96c6362f73a220825836e08e51e2e6109799c9 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:54:20 +0100 Subject: [PATCH 48/89] prefs.py: Initial LANG comment pass Pausing for a possible bug fix --- prefs.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/prefs.py b/prefs.py index 31c5f6b6..2a7ed3ac 100644 --- a/prefs.py +++ b/prefs.py @@ -294,6 +294,7 @@ class PreferencesDialog(tk.Toplevel): buttonframe.grid(padx=self.PADX, pady=self.PADX, sticky=tk.NSEW) 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.grid(row=0, column=1, sticky=tk.E) button.bind("<Return>", lambda event: self.apply()) @@ -378,6 +379,7 @@ class PreferencesDialog(tk.Toplevel): self.outdir = tk.StringVar() self.outdir.set(str(config.get_str('outdir'))) + # LANG: Label for "where files are located" self.outdir_label = nb.Label(output_frame, text=_('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=(5, 0), sticky=tk.W, row=row.get()) # type: ignore @@ -394,7 +396,8 @@ class PreferencesDialog(tk.Toplevel): nb.Frame(output_frame).grid(row=row.get()) # bottom spacer # TODO: does nothing? - root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings + # LANG: Label for 'Output' Settings tab + root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings def __setup_plugin_tabs(self, notebook: Notebook) -> None: for plugin in plug.PLUGINS: @@ -451,9 +454,9 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_play = tk.IntVar(value=not config.get_int('hotkey_mute')) nb.Label( config_frame, - text=_('Keyboard shortcut') if # Hotkey/Shortcut settings prompt on OSX + text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX platform == 'darwin' else - _('Hotkey') # Hotkey/Shortcut settings prompt on Windows + _('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows ).grid(padx=self.PADX, sticky=tk.W, row=row.get()) if platform == 'darwin' and not was_accessible_at_launch: @@ -473,7 +476,7 @@ class PreferencesDialog(tk.Toplevel): foreground='firebrick' ).grid(columnspan=4, padx=self.PADX, sticky=tk.W, row=row.get()) - # Shortcut settings button on OSX + # LANG: Shortcut settings button on OSX nb.Button(config_frame, text=_('Open System Preferences'), command=self.enableshortcuts).grid( padx=self.PADX, sticky=tk.E, row=row.get() ) @@ -484,6 +487,7 @@ class PreferencesDialog(tk.Toplevel): 0, # No hotkey/shortcut currently defined # TODO: display Only shows up on darwin or windows + # LANG: No hotkey/shortcut set hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None') ) @@ -530,6 +534,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( columnspan=4, padx=self.PADX, sticky=tk.W, row=row.get() ) @@ -540,6 +545,7 @@ class PreferencesDialog(tk.Toplevel): value=str(shipyard_provider if shipyard_provider in plug.provides('shipyard_url') else 'EDSY') ) # 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=2*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') @@ -551,6 +557,7 @@ class PreferencesDialog(tk.Toplevel): self.alt_shipyard_open = tk.IntVar(value=config.get_int('use_alt_shipyard_open')) self.alt_shipyard_open_btn = nb.Checkbutton( config_frame, + # LANG: Label for checkbox to utilise alternative Coriolis URL method text=_('Use alternate URL method'), variable=self.alt_shipyard_open, command=self.alt_shipyard_open_changed, @@ -628,18 +635,22 @@ class PreferencesDialog(tk.Toplevel): # Big spacer nb.Label(config_frame).grid(sticky=tk.W, row=row.get()) - notebook.add(config_frame, text=_('Configuration')) # Tab heading in settings + # LANG: Label for 'Configuration' tab in Settings + notebook.add(config_frame, text=_('Configuration')) def __setup_appearance_tab(self, notebook: Notebook) -> None: self.languages = Translations.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.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 + # LANG: Label for Settings > Appeareance > selection of 'highlightes' text colour _('Highlighted text'), # Dark theme color setting ] @@ -657,21 +668,25 @@ class PreferencesDialog(tk.Toplevel): ) # Appearance setting + # LANG: Label for Settings > Appearance > Theme selection nb.Label(appearance_frame, text=_('Theme')).grid(columnspan=3, padx=self.PADX, 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, value=0, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, 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=1, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) if platform == 'win32': nb.Radiobutton( appearance_frame, + # LANG: Label for 'Transparent' theme radio button text=_('Transparent'), # Appearance theme setting variable=self.theme, value=2, @@ -801,6 +816,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 def __setup_plugin_tab(self, notebook: Notebook) -> None: @@ -812,6 +828,7 @@ class PreferencesDialog(tk.Toplevel): row = AutoInc(1) # Section heading in settings + # LANG: Label for nb.Label(plugins_frame, text=_('Plugins folder')+':').grid(padx=self.PADX, sticky=tk.W) plugdirentry = nb.Entry(plugins_frame, justify=tk.LEFT) self.displaypath(plugdir, plugdirentry) From 1495ba920651982afdf958f6742afbfd71ea4342 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 15:56:50 +0100 Subject: [PATCH 49/89] prefs.py: Properly .grid() the 'Plugins folder' label --- prefs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/prefs.py b/prefs.py index 2a7ed3ac..a01a34d7 100644 --- a/prefs.py +++ b/prefs.py @@ -827,12 +827,13 @@ class PreferencesDialog(tk.Toplevel): plugdir.set(config.plugin_dir) row = AutoInc(1) - # Section heading in settings - # LANG: Label for - nb.Label(plugins_frame, text=_('Plugins folder')+':').grid(padx=self.PADX, sticky=tk.W) plugdirentry = nb.Entry(plugins_frame, justify=tk.LEFT) self.displaypath(plugdir, plugdirentry) with row as cur_row: + # Section heading in settings + # LANG: Label for location of third-party plugins folder + nb.Label(plugins_frame, text=_('Plugins folder') + ':').grid(padx=self.PADX, sticky=tk.W, row=cur_row) + plugdirentry.grid(padx=self.PADX, sticky=tk.EW, row=cur_row) nb.Button( From f4af278a805013b8d4f39925e73b301dd59d5fba Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:00:28 +0100 Subject: [PATCH 50/89] prefs.py: Contiuning LANG comment pass --- prefs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/prefs.py b/prefs.py index a01a34d7..ab067b6d 100644 --- a/prefs.py +++ b/prefs.py @@ -838,6 +838,7 @@ class PreferencesDialog(tk.Toplevel): nb.Button( 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}') ).grid(column=1, padx=(0, self.PADX), sticky=tk.NSEW, row=cur_row) @@ -845,6 +846,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( 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') ).grid(columnspan=2, padx=self.PADX, pady=10, sticky=tk.NSEW, row=row.get()) @@ -855,6 +857,7 @@ class PreferencesDialog(tk.Toplevel): ) nb.Label( plugins_frame, + # LANG: Label on list of enabled plugins text=_('Enabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, sticky=tk.W, row=row.get()) @@ -895,6 +898,7 @@ class PreferencesDialog(tk.Toplevel): ) nb.Label( plugins_frame, + # LANG: Lable on list of user-disabled plugins text=_('Disabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, sticky=tk.W, row=row.get()) @@ -903,6 +907,7 @@ class PreferencesDialog(tk.Toplevel): columnspan=2, padx=self.PADX*2, sticky=tk.W, row=row.get() ) + # LANG: Label on Settings > Plugins tab notebook.add(plugins_frame, text=_('Plugins')) # Tab heading in settings def cmdrchanged(self, event=None): @@ -1105,7 +1110,7 @@ class PreferencesDialog(tk.Toplevel): event.widget.delete(0, tk.END) self.hotkey_text.insert( 0, - # No hotkey/shortcut currently defined + # LANG: No hotkey/shortcut set hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')) def hotkeylisten(self, event: 'tk.Event[Any]') -> str: @@ -1138,7 +1143,8 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_play_btn['state'] = tk.NORMAL else: - event.widget.insert(0, _('None')) # No hotkey/shortcut currently defined + # LANG: No hotkey/shortcut set + event.widget.insert(0, _('None')) self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED From 631446f619642be76510068f69beec2e6f8af371 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:02:39 +0100 Subject: [PATCH 51/89] Translations: Final LANG comments The script now outputs nothing about Unknown comments --- EDMarketConnector.py | 2 +- theme.py | 1 + ttkHyperlinkLabel.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index b3e48b00..cd75a2bf 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -702,7 +702,7 @@ class AppWindow(object): def set_labels(self): """Set main window labels, e.g. after language change.""" - self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Main window + self.cmdr_label['text'] = _('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: Main window diff --git a/theme.py b/theme.py index 702edf3c..cac65ba7 100644 --- a/theme.py +++ b/theme.py @@ -223,6 +223,7 @@ class _Theme(object): 'disabledforeground' : '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)), 'highlight' : config.get_str('dark_highlight'), # 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 tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or 'TkDefaultFont'), diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index c731ffac..2f64ab74 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -38,7 +38,8 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object): self.bind('<Button-1>', self._click) self.menu = tk.Menu(None, tearoff=tk.FALSE) - self.menu.add_command(label=_('Copy'), command = self.copy) # As in Copy and Paste + # LANG: Label for 'Copy' as in 'Copy and Paste' + self.menu.add_command(label=_('Copy'), command = self.copy) # As in Copy and Paste self.bind(platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu) self.bind('<Enter>', self._enter) From 9e8bd1926b37d35789f618d04679c1db2396e416 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:37:53 +0100 Subject: [PATCH 52/89] pre-commit: Run `scripts/find_localised_strings.py` This will report on any missing LANG comments at commit time. NB: set to run on all files in case of L10n/en.template changes. --- .pre-commit-config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25f65352..8c517ecf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,6 +66,15 @@ repos: args: [check, --bare, -r] language: system +# Check translation comments are up to date +- repo: local + hooks: + - id: LANG_comments + name: 'LANG comments' + language: system + entry: python scripts/find_localised_strings.py --compare-lang L10n/en.template --directory . --ignore coriolis-data --ignore dist.win32 + pass_filenames: false + default_language_version: python: python3.9 From 3f3e007305eac9e1fe4f98869c6ef9168bec724c Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:39:19 +0100 Subject: [PATCH 53/89] pre-commit: *Ensure* the LANG_comments always runs --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c517ecf..a8bfb90c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,7 @@ repos: language: system entry: python scripts/find_localised_strings.py --compare-lang L10n/en.template --directory . --ignore coriolis-data --ignore dist.win32 pass_filenames: false + always_run: true default_language_version: python: python3.9 From 2de39f60ab39ace8a996a8b73f44fe8d89127988 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:50:28 +0100 Subject: [PATCH 54/89] l10n script: Complain if there's no current/prev LANG comment --- scripts/find_localised_strings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 3f1e347c..ac38c263 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -93,6 +93,9 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op if bad_comment is not None: print(bad_comment, file=sys.stderr) + if out is None: + print(f'No comment for {file}:{current} {line}') + return out From eb8b357e2728078785a122d8a7d8085e734bec40 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:51:08 +0100 Subject: [PATCH 55/89] l10n script: 'No comment for' on stderr --- scripts/find_localised_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index ac38c263..60b1a03e 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -94,7 +94,7 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Op print(bad_comment, file=sys.stderr) if out is None: - print(f'No comment for {file}:{current} {line}') + print(f'No comment for {file}:{current} {line}', file=sys.stderr) return out From efbc51c7142bbb39dd370f332f90950ac00fa759 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Mon, 7 Jun 2021 16:56:53 +0100 Subject: [PATCH 56/89] EDMarketConnector: Improve legacy LANG comments --- EDMarketConnector.py | 62 ++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index cd75a2bf..49984ea2 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -705,43 +705,43 @@ class AppWindow(object): self.cmdr_label['text'] = _('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: Main window - self.system_label['text'] = _('System') + ':' # LANG: Main window - self.station_label['text'] = _('Station') + ':' # LANG: 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 if platform == 'darwin': - self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title on OSX - self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title on OSX - self.menubar.entryconfigure(3, label=_('View')) # LANG: Menu title on OSX - self.menubar.entryconfigure(4, label=_('Window')) # LANG: Menu title on OSX - self.menubar.entryconfigure(5, label=_('Help')) # LANG: Menu title on OSX + self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title on OSX + self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title on OSX + self.menubar.entryconfigure(3, label=_('View')) # LANG: 'View' menu title on OSX + self.menubar.entryconfigure(4, label=_('Window')) # LANG: 'Window' menu title on OSX + self.menubar.entryconfigure(5, label=_('Help')) # LANG: Help' menu title on OSX self.system_menu.entryconfigure(0, label=_("About {APP}").format( APP=applongname)) # LANG: App menu entry on OSX - self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Menu item - self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: Menu item - self.view_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item + self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Help > Check for Updates + self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: File > Save Raw Data... + self.view_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help > Privacy Policy + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help > Release Notes else: - self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title - self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title - self.menubar.entryconfigure(3, label=_('Help')) # LANG: Menu title - self.theme_file_menu['text'] = _('File') # LANG: Menu title - self.theme_edit_menu['text'] = _('Edit') # LANG: Menu title - self.theme_help_menu['text'] = _('Help') # LANG: Menu title + 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 # File menu - self.file_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item - self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: Menu item - self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: Item in the File menu on Windows - self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: Item in the File menu on Windows + 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 (Windows) + self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: File > Exit # Help menu - self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item - self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # LANG: Menu item - self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # LANG: App menu entry + self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help > Documentation + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help > Privacy Policy + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help > Release Notes + self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # LANG: Help > Check for Updates... + self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # LANG: Help > About App # Edit menu self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: As in Copy and Paste @@ -1042,7 +1042,7 @@ class AppWindow(object): else: self.cmdr['text'] = monitor.cmdr - self.ship_label['text'] = _('Ship') + ':' # LANG: Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI # TODO: Show something else when on_foot if monitor.state['ShipName']: @@ -1065,7 +1065,7 @@ class AppWindow(object): else: self.cmdr['text'] = '' - self.ship_label['text'] = _('Ship') + ':' # LANG: Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI self.ship['text'] = '' if monitor.cmdr and monitor.is_beta: @@ -1739,7 +1739,7 @@ sys.path: {sys.path}''' ) # Substitute in the other words. - # LANG: words for use in python 2 plugin error + # LANG: 'Plugins' tab / 'File' menu / 'File' > 'Settings' popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled') # And now we do need these to be actual \r\n From e6c8183bfc1a22d975384d547c7b8f41ad7653e1 Mon Sep 17 00:00:00 2001 From: A_D <aunderscored@gmail.com> Date: Fri, 4 Jun 2021 17:44:51 +0200 Subject: [PATCH 57/89] made EDDN errors more verbose --- plugins/eddn.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/eddn.py b/plugins/eddn.py index 693f01fc..1401b0c8 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -255,6 +255,9 @@ Msg:\n{msg}''' if not len(self.replaylog) % self.REPLAYFLUSH: self.flush() + except requests.exceptions.HTTPError as e: + status['text'] = self.http_error_to_log(e) + except requests.exceptions.RequestException as e: logger.debug('Failed sending', exc_info=e) status['text'] = _("Error: Can't connect to EDDN") @@ -267,6 +270,24 @@ Msg:\n{msg}''' self.parent.after(self.REPLAYPERIOD, self.sendreplay) + @staticmethod + def http_error_to_log(exception: requests.exceptions.HTTPError) -> str: + """Convert an exception from raise_for_status to a log message and displayed error.""" + status_code = exception.errno + + if status_code == 429: # HTTP UPGRADE REQUIRED + logger.warning('EDMC is sending schemas that are too old') + return _("EDDN Error: EDMC is too old for EDDN. Please update.") + + elif status_code == 400: + # we a validation check or something else. + logger.warning(f'EDDN Error: {status_code} -- {exception.response}') + return _("EDDN Error: Validation Failed (EDMC Too Old?). See Log") + + else: + logger.warning(f'Unknown status code from EDDN: {status_code} -- {exception.response}') + return _("EDDN Error: Returned {STATUS} status code").format(status_code) + def export_commodities(self, data: Mapping[str, Any], is_beta: bool, is_odyssey: bool) -> None: # noqa: CCR001 """ Update EDDN with the commodities on the current (lastStarport) station. From 17980ac6ad97072ee5f035df749b9333079c3066 Mon Sep 17 00:00:00 2001 From: A_D <aunderscored@gmail.com> Date: Fri, 4 Jun 2021 17:50:30 +0200 Subject: [PATCH 58/89] Added localisation --- L10n/en.template | 9 +++++++++ plugins/eddn.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/L10n/en.template b/L10n/en.template index 3be9753f..c3e2939e 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -621,3 +621,12 @@ /* stats.py: Status dialog title; In files: stats.py:375; */ "Ships" = "Ships"; + +/* EDDN returned schema too old warning [plugins/eddn.py] */ +"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Error: EDMC is too old for EDDN. Please update."; + +/* EDDN returned 400 status code [plugins/eddn.py] */ +"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Error: Validation Failed (EDMC Too Old?). See Log"; + +/* EDDN returned unknown HTTP status code [plugins/eddn.py] */ +"EDDN Error: Returned {STATUS} status code" = "EDDN Error: Returned {STATUS} status code"; diff --git a/plugins/eddn.py b/plugins/eddn.py index 1401b0c8..8f798586 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -277,16 +277,16 @@ Msg:\n{msg}''' if status_code == 429: # HTTP UPGRADE REQUIRED logger.warning('EDMC is sending schemas that are too old') - return _("EDDN Error: EDMC is too old for EDDN. Please update.") + return _('EDDN Error: EDMC is too old for EDDN. Please update.') elif status_code == 400: # we a validation check or something else. logger.warning(f'EDDN Error: {status_code} -- {exception.response}') - return _("EDDN Error: Validation Failed (EDMC Too Old?). See Log") + return _('EDDN Error: Validation Failed (EDMC Too Old?). See Log') else: logger.warning(f'Unknown status code from EDDN: {status_code} -- {exception.response}') - return _("EDDN Error: Returned {STATUS} status code").format(status_code) + return _('EDDN Error: Returned {STATUS} status code').format(status_code) def export_commodities(self, data: Mapping[str, Any], is_beta: bool, is_odyssey: bool) -> None: # noqa: CCR001 """ From c4e9767974187523fad399c99a1a29269ee1c15f Mon Sep 17 00:00:00 2001 From: A_D <aunderscored@gmail.com> Date: Fri, 21 May 2021 08:17:55 +0200 Subject: [PATCH 59/89] Don't spam a stacktrace on requests.ConnectionError ConnectionErrors are expected and not something we can fix. The exception handler that was catching these previously is a catchall intended to stop EDMC as a whole from crashing from something unexpected. Closes #1082 --- EDMarketConnector.py | 5 +++++ companion.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 49984ea2..2d79357d 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -979,6 +979,11 @@ class AppWindow(object): companion.session.invalidate() self.login() + except companion.ServerConnectionError as e: + logger.debug(f'Exception while contacting server: {e}') + err = self.status['text'] = str(e) + play_bad = True + except Exception as e: # Including CredentialsError, ServerError logger.debug('"other" exception', exc_info=e) err = self.status['text'] = str(e) diff --git a/companion.py b/companion.py index d374f169..3eeedf23 100644 --- a/companion.py +++ b/companion.py @@ -17,6 +17,7 @@ import random import time import urllib.parse import webbrowser +import socket from builtins import object, range, str from email.utils import parsedate from os.path import join @@ -170,6 +171,10 @@ class ServerError(Exception): self.args = (_("Error: Frontier CAPI didn't respond"),) +class ServerConnectionError(ServerError): + """Exception class for CAPI connection errors.""" + + class ServerLagging(Exception): """Exception Class for CAPI Server lagging. @@ -537,6 +542,10 @@ class Session(object): logger.trace('Trying...') r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore + except requests.ConnectionError as e: + logger.debug(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})') + raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e + except Exception as e: logger.debug('Attempting GET', exc_info=e) raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e From a79cde641e6daf3872c74eb86bda26a317e8f9b9 Mon Sep 17 00:00:00 2001 From: A_D <aunderscored@gmail.com> Date: Fri, 21 May 2021 10:59:43 +0200 Subject: [PATCH 60/89] Removed unused import --- companion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/companion.py b/companion.py index 3eeedf23..1095350a 100644 --- a/companion.py +++ b/companion.py @@ -17,7 +17,6 @@ import random import time import urllib.parse import webbrowser -import socket from builtins import object, range, str from email.utils import parsedate from os.path import join From 09a0b708913c1adb2e966805d1504d3b15ea17a9 Mon Sep 17 00:00:00 2001 From: A_D <aunderscored@gmail.com> Date: Mon, 7 Jun 2021 19:07:24 +0200 Subject: [PATCH 61/89] Switched logs to warning --- EDMarketConnector.py | 2 +- companion.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 2d79357d..93869299 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -980,7 +980,7 @@ class AppWindow(object): self.login() except companion.ServerConnectionError as e: - logger.debug(f'Exception while contacting server: {e}') + logger.warning(f'Exception while contacting server: {e}') err = self.status['text'] = str(e) play_bad = True diff --git a/companion.py b/companion.py index 1095350a..f8cf6c26 100644 --- a/companion.py +++ b/companion.py @@ -542,7 +542,7 @@ class Session(object): r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore except requests.ConnectionError as e: - logger.debug(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})') + logger.warning(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})') raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e except Exception as e: From fb5d5a736cbda9c99a3f6a4c28a8089f46d24848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 05:28:32 +0000 Subject: [PATCH 62/89] build(deps-dev): bump mypy from 0.812 to 0.901 Bumps [mypy](https://github.com/python/mypy) from 0.812 to 0.901. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.812...v0.901) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6320168..7cb35f6c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,7 @@ flake8-noqa==1.1.0 flake8-polyfill==1.0.2 flake8-use-fstring==1.1 -mypy==0.812 +mypy==0.901 pep8-naming==0.11.1 safety==1.10.3 From 83358f87e1c21c0ce91ccee096857e38d3ec832b Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Wed, 9 Jun 2021 08:26:40 +0100 Subject: [PATCH 63/89] requirements.txt: mypy 0.900+ needs separate types-requests --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7cb35f6c..e361dce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,6 +18,7 @@ flake8-use-fstring==1.1 mypy==0.901 pep8-naming==0.11.1 safety==1.10.3 +types-requests==0.1.8 # Code formatting tools autopep8==1.5.7 From 452528ae87f812d89c2e6e12e720cd8ad0a4c616 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jun 2021 05:18:22 +0000 Subject: [PATCH 64/89] build(deps-dev): bump types-requests from 0.1.8 to 0.1.9 Bumps [types-requests](https://github.com/python/typeshed) from 0.1.8 to 0.1.9. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e361dce4..93916d45 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ flake8-use-fstring==1.1 mypy==0.901 pep8-naming==0.11.1 safety==1.10.3 -types-requests==0.1.8 +types-requests==0.1.9 # Code formatting tools autopep8==1.5.7 From 7934754482ec32299a66c25fb3cf2dfde5a4359c Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:12:02 +0100 Subject: [PATCH 65/89] ShipLocker: New event that replaces ShipLockerMaterials On startup, embark, disembark this is a full event, otherwise it signals writing of new ShipLocker.json file. --- monitor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/monitor.py b/monitor.py index 4265496e..22893aff 100644 --- a/monitor.py +++ b/monitor.py @@ -843,7 +843,21 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # So it's *from* the ship self.state['Cargo'][name] -= c['Count'] - elif event_type == 'ShipLockerMaterials': + elif event_type == 'ShipLocker': + # As of 4.0.0.400 (2021-06-10) + # "ShipLocker" will be a full list written to the journal at startup/boarding/disembarking, and also + # written to a separate shiplocker.json file - other updates will just update that file and mention it + # has changed with an empty shiplocker event in the main journal. + + if not all([entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')]): + logger.debug('ShipLocker event is an empty one (at least missing one data type') + # So attempt to load data from the most recent file instead + currentdir_path = pathlib.Path(str(self.currentdir)) + # Confirmed filename for 4.0.0.400 + with open(currentdir_path / 'ShipLocker.json', 'rb') as h: # type: ignore + entry = json.load(h, object_pairs_hook=OrderedDict) + self.state['ShipLockerJSON'] = entry + # This event has the current totals, so drop any current data self.state['Component'] = defaultdict(int) self.state['Consumable'] = defaultdict(int) From 2b1ca8031c0bd788a283fe3834776631e486d2ae Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:16:14 +0100 Subject: [PATCH 66/89] Backpack: Still don't need to worry about UseConsumable in 4.0.0.400 --- monitor.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/monitor.py b/monitor.py index 22893aff..6bb21e62 100644 --- a/monitor.py +++ b/monitor.py @@ -1106,21 +1106,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # suit backpack – note this can be written at the same time as other # events like UseConsumable - # In 4.0.0.100 it is observed that: - # - # 1. Throw of any grenade type *only* causes a BackpackChange event, no - # accompanying 'UseConsumable'. - # 2. Using an Energy Cell causes both UseConsumable and BackpackChange, - # in that order. - # 3. Medkit acts the same as Energy Cell. - # - # Thus we'll just ignore 'UseConsumable' for now. - # for c in self.state['BackPack']['Consumable']: - # if c == entry['Name']: - # self.state['BackPack']['Consumable'][c] -= 1 - # # Paranoia in case we lost track - # if self.state['BackPack']['Consumable'][c] < 0: - # self.state['BackPack']['Consumable'][c] = 0 + # In 4.0.0.400 we do get this event, but *also* a `BackpackChange` event, + # so we ignore this for inventory purposes. pass # TODO: From 8f05282d6352589da1cfdf2a186be03df796ef70 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:17:15 +0100 Subject: [PATCH 67/89] Backpack: 4.0.0.400 - can still ignore `CollectItems` --- monitor.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/monitor.py b/monitor.py index 6bb21e62..19cebadc 100644 --- a/monitor.py +++ b/monitor.py @@ -1068,17 +1068,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['BackPack'][c][m] = 0 elif event_type == 'CollectItems': - # alpha4 - # When picking up items from the ground - # Parameters: - # • Name - # • Type - # • OwnerID - - # Handled by BackpackChange - # for i in self.state['BackPack'][entry['Type']]: - # if i == entry['Name']: - # self.state['BackPack'][entry['Type']][i] += entry['Count'] + # 4.0.0.400 (still) has a BackpackChange event as well, so + # ignore this for inventory purposes. pass elif event_type == 'DropItems': From f852a185e408c8486eb94b955364c898b90f717c Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:18:00 +0100 Subject: [PATCH 68/89] Backpack: 4.0.0.400 - Can still ignore this for inventory purposes --- monitor.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/monitor.py b/monitor.py index 19cebadc..0758cbd4 100644 --- a/monitor.py +++ b/monitor.py @@ -1073,21 +1073,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below pass elif event_type == 'DropItems': - # alpha4 - # Parameters: - # • Name - # • Type - # • OwnerID - # • MissionID - # • Count - - # This is handled by BackpackChange. - # for i in self.state['BackPack'][entry['Type']]: - # if i == entry['Name']: - # self.state['BackPack'][entry['Type']][i] -= entry['Count'] - # # Paranoia in case we lost track - # if self.state['BackPack'][entry['Type']][i] < 0: - # self.state['BackPack'][entry['Type']][i] = 0 + # 4.0.0.400 (still) has a BackpackChange event as well, so + # ignore this for inventory purposes. pass elif event_type == 'UseConsumable': From c3a46a841570c694c229b4661b027d110aa5619a Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:19:35 +0100 Subject: [PATCH 69/89] ShipLocker: 4.0.0.400 - can ignore `BuyMicroResources` for inventory purposes As the new `ShipLocker` is present in 'empty' form, with full new inventory in the file. --- monitor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/monitor.py b/monitor.py index 0758cbd4..8b0f370e 100644 --- a/monitor.py +++ b/monitor.py @@ -1016,12 +1016,10 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['BackPack'][c][m] = 0 elif event_type == 'BuyMicroResources': - # Buying from a Pioneer Supplies, goes directly to ShipLocker. - # One event per Item, not an array. - category = self.category(entry['Category']) - name = self.canonicalise(entry['Name']) - self.state[category][name] += entry['Count'] + # From 4.0.0.400 we get an empty (see file) `ShipLocker` event, + # so we can ignore this for inventory purposes. + # But do record the credits balance change. self.state['Credits'] -= entry.get('Price', 0) elif event_type == 'SellMicroResources': From db43442504f377183e3d4b7e2d6ba88de6481d27 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:21:40 +0100 Subject: [PATCH 70/89] ShipLocker: Ignore `TradeMicroResources` for inventory purposes We get `ShipLocker`/file with full new inventory. --- monitor.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/monitor.py b/monitor.py index 8b0f370e..db01fbe3 100644 --- a/monitor.py +++ b/monitor.py @@ -1032,17 +1032,9 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state[category][name] -= mr['Count'] elif event_type == 'TradeMicroResources': - # Trading some MicroResources for another at a Bar Tender - # 'Offered' is what we traded away - for offer in entry['Offered']: - category = self.category(offer['Category']) - name = self.canonicalise(offer['Name']) - self.state[category][name] -= offer['Count'] - - # For a single item name received - category = self.category(entry['Category']) - name = self.canonicalise(entry['Received']) - self.state[category][name] += entry['Count'] + # As of 4.0.0.400 we can ignore this as an empty (see file) + # `ShipLocker` event is written for the full new inventory. + pass elif event_type == 'TransferMicroResources': # Moving Odyssey MicroResources between ShipLocker and BackPack From 59b410341ccbab341747a567e4efb95b6ec431a7 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:22:56 +0100 Subject: [PATCH 71/89] ShipLocker: Can now ignore `SellMicroResources` for inventory purposes `ShipLocker` empty/file event has full new inventory. --- monitor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/monitor.py b/monitor.py index db01fbe3..25001eb2 100644 --- a/monitor.py +++ b/monitor.py @@ -1023,13 +1023,11 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['Credits'] -= entry.get('Price', 0) elif event_type == 'SellMicroResources': - # Selling to a Bar Tender on-foot. + # As of 4.0.0.400 we can ignore this as an empty (see file) + # `ShipLocker` event is written for the full new inventory. + + # But still record the credits balance change. self.state['Credits'] += entry.get('Price', 0) - # One event per whole sale, so it's an array. - for mr in entry['MicroResources']: - category = self.category(mr['Category']) - name = self.canonicalise(mr['Name']) - self.state[category][name] -= mr['Count'] elif event_type == 'TradeMicroResources': # As of 4.0.0.400 we can ignore this as an empty (see file) From c2d75e7f1dcc59d2f144def42d617ea89dae1e1b Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:24:13 +0100 Subject: [PATCH 72/89] Credits: UpgradeWeapon should now include a credits cost. Need to see 4.0.0.400 event for this and UpgradeSuit to be sure they don't also cite materials costs. But the hope is there'd be a new empty/file `ShipLocker` event anyway. --- monitor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monitor.py b/monitor.py index 25001eb2..ff08c3eb 100644 --- a/monitor.py +++ b/monitor.py @@ -1288,8 +1288,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below elif event_type == 'UpgradeWeapon': # We're not actually keeping track of all owned weapons, only those in # Suit Loadouts. - # alpha4 - credits? Shouldn't cost any! - pass + self.state['Credits'] -= entry.get('Cost', 0) elif event_type == 'ScanOrganic': # Nothing of interest to our state. From f942e763137168c2f5ca1e2c2068f24cdd7269e4 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:28:37 +0100 Subject: [PATCH 73/89] monitor: `BackPackMaterials` shouldn't be a thing any more --- monitor.py | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/monitor.py b/monitor.py index ff08c3eb..eaa36dfd 100644 --- a/monitor.py +++ b/monitor.py @@ -895,34 +895,9 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # Journal v31 implies this was removed before Odyssey launch elif event_type == 'BackPackMaterials': - # alpha4 - - # Lists the contents of the backpack, eg when disembarking from ship - - # Assume this reflects the current state when written - self.state['BackPack']['Component'] = defaultdict(int) - self.state['BackPack']['Consumable'] = defaultdict(int) - self.state['BackPack']['Item'] = defaultdict(int) - self.state['BackPack']['Data'] = defaultdict(int) - - clean_components = self.coalesce_cargo(entry['Components']) - self.state['BackPack']['Component'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_components} - ) - - clean_consumables = self.coalesce_cargo(entry['Consumables']) - self.state['BackPack']['Consumable'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_consumables} - ) - - clean_items = self.coalesce_cargo(entry['Items']) - self.state['BackPack']['Item'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_items} - ) - - clean_data = self.coalesce_cargo(entry['Data']) - self.state['BackPack']['Data'].update( - {self.canonicalise(x['Name']): x['Count'] for x in clean_data} - ) + # Last seen in a 4.0.0.102 journal file. + logger.warning(f'We have a BackPackMaterials event, defunct since > 4.0.0.102 ?:\n{entry}\n') + pass elif event_type in ('BackPack', 'Backpack'): # WORKAROUND 4.0.0.200: BackPack becomes Backpack # TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley From b02729ebdc816b22b8677c0565741e09202cd5b9 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:32:40 +0100 Subject: [PATCH 74/89] Backpack/ShipLocker: `TransferMicroResources` is gone as of 4.0.0.400 --- monitor.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/monitor.py b/monitor.py index eaa36dfd..8deaa5b3 100644 --- a/monitor.py +++ b/monitor.py @@ -1010,25 +1010,10 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below pass elif event_type == 'TransferMicroResources': - # Moving Odyssey MicroResources between ShipLocker and BackPack - # Backpack dropped as its done in BackpackChange - # - # from: 4.0.0.200 -- Locker(Old|New)Count is now a thing. - for mr in entry['Transfers']: - category = self.category(mr['Category']) - name = self.canonicalise(mr['Name']) - - self.state[category][name] = mr['LockerNewCount'] - if mr['Direction'] not in ('ToShipLocker', 'ToBackpack'): - logger.warning(f'TransferMicroResources with unexpected Direction {mr["Direction"]=}: {mr=}') - - # Paranoia check to see if anything has gone negative. - # As of Odyssey Alpha Phase 1 Hotfix 2 keeping track of BackPack - # materials is impossible when used/picked up anyway. - for c in self.state['BackPack']: - for m in self.state['BackPack'][c]: - if self.state['BackPack'][c][m] < 0: - self.state['BackPack'][c][m] = 0 + # Defunct in 4.0.0.400 ? Not seen in testing, and we get a + # new empty/file `ShipLocker` event, along with a + # `BackpackChange` event per item type transferred. + pass elif event_type == 'CollectItems': # 4.0.0.400 (still) has a BackpackChange event as well, so From 09c81a21f684ee8df55a566af931a501a4ec2c97 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:34:30 +0100 Subject: [PATCH 75/89] ShipLocker: do *NOT* zero out BackPack when handling this event Just need to ensure we do on Embark (or Death? but likely fresh events then anyway?). Certainly if Journal event order is ever Backpack *then* ShipLocker this would erroneously zero out the backpack. --- monitor.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/monitor.py b/monitor.py index 8deaa5b3..4f7ebce9 100644 --- a/monitor.py +++ b/monitor.py @@ -863,15 +863,10 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['Consumable'] = defaultdict(int) self.state['Item'] = defaultdict(int) self.state['Data'] = defaultdict(int) - # TODO: Really we need a full BackPackMaterials event at the same time. - # In lieu of that, empty the backpack. This will explicitly - # be wrong if Cmdr relogs at a Settlement with anything in - # backpack. - # Still no BackPackMaterials at the same time in 4.0.0.31 - self.state['BackPack']['Component'] = defaultdict(int) - self.state['BackPack']['Consumable'] = defaultdict(int) - self.state['BackPack']['Item'] = defaultdict(int) - self.state['BackPack']['Data'] = defaultdict(int) + + # 4.0.0.400 - No longer zeroing out the BackPack in this event, + # as we should now always get either `Backpack` event/file or + # `BackpackChange` as needed. clean_components = self.coalesce_cargo(entry['Components']) self.state['Component'].update( From 8b6c0e50ace42da1ca504d342e03480707c0cb41 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:37:59 +0100 Subject: [PATCH 76/89] Backpack: Zero out when we Embark, as it's all then in ShipLocker --- monitor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monitor.py b/monitor.py index 4f7ebce9..bbfbc370 100644 --- a/monitor.py +++ b/monitor.py @@ -681,6 +681,13 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['OnFoot'] = False self.state['Taxi'] = entry['Taxi'] + # We can't now have anything in the BackPack, it's all in the + # ShipLocker. + self.state['BackPack']['Component'] = defaultdict(int) + self.state['BackPack']['Consumable'] = defaultdict(int) + self.state['BackPack']['Item'] = defaultdict(int) + self.state['BackPack']['Data'] = defaultdict(int) + elif event_type == 'Disembark': # This event is logged when the player steps out of a ship or SRV # From ae18071f139f871bd8615d2d7c9cfc2186eb85c1 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:39:53 +0100 Subject: [PATCH 77/89] Backpack: Factor out the "make it empty" code. --- monitor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/monitor.py b/monitor.py index bbfbc370..05c6cc8d 100644 --- a/monitor.py +++ b/monitor.py @@ -683,10 +683,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # We can't now have anything in the BackPack, it's all in the # ShipLocker. - self.state['BackPack']['Component'] = defaultdict(int) - self.state['BackPack']['Consumable'] = defaultdict(int) - self.state['BackPack']['Item'] = defaultdict(int) - self.state['BackPack']['Data'] = defaultdict(int) + self.backpack_set_empty() elif event_type == 'Disembark': # This event is logged when the player steps out of a ship or SRV @@ -934,10 +931,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['BackpackJSON'] = entry # Assume this reflects the current state when written - self.state['BackPack']['Component'] = defaultdict(int) - self.state['BackPack']['Consumable'] = defaultdict(int) - self.state['BackPack']['Item'] = defaultdict(int) - self.state['BackPack']['Data'] = defaultdict(int) + self.backpack_set_empty() clean_components = self.coalesce_cargo(entry['Components']) self.state['BackPack']['Component'].update( @@ -1588,6 +1582,13 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below logger.debug(f'Invalid journal entry:\n{line!r}\n', exc_info=ex) return {'event': None} + def backpack_set_empty(self): + """Set the BackPack contents to be empty.""" + self.state['BackPack']['Component'] = defaultdict(int) + self.state['BackPack']['Consumable'] = defaultdict(int) + self.state['BackPack']['Item'] = defaultdict(int) + self.state['BackPack']['Data'] = defaultdict(int) + def suit_sane_name(self, name: str) -> str: """ Given an input suit name return the best 'sane' name we can. From 3ea0bcce5e51b5c26cf0b1dfefda1130712c0465 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:44:46 +0100 Subject: [PATCH 78/89] BackPack: No need to worry about `Resurrect`. --- monitor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monitor.py b/monitor.py index 05c6cc8d..abc9d952 100644 --- a/monitor.py +++ b/monitor.py @@ -1571,6 +1571,9 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below elif event_type == 'Resurrect': self.state['Credits'] -= entry.get('Cost', 0) + # There should be a `Backpack` event as you 'come to' in the + # new location, so no need to zero out BackPack here. + # HACK (not game related / 2021-06-2): self.planet is moved into a more general self.state['Body']. # This exists to help plugins doing what they SHOULDN'T BE cope. It will be removed at some point. if self.state['Body'] is None or self.state['BodyType'] == 'Planet': From 9fa02b51c326b3f393195d23c77701fb839762ab Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 12:52:15 +0100 Subject: [PATCH 79/89] Inara: Update to use `ShipLocker`, possibly from file. --- plugins/inara.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/inara.py b/plugins/inara.py index f4db0098..ee43a592 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -1098,7 +1098,14 @@ def journal_entry( # noqa: C901, CCR001 new_add_event('addCommanderTravelLand', entry['timestamp'], to_send_data) - elif event_name == 'ShipLockerMaterials': + elif event_name == 'ShipLocker': + # In ED 4.0.0.400 the event is only full sometimes, other times indicating + # ShipLocker.json was written. + if not all([entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')]): + # So it's an empty event, core EDMC should have stuffed the data + # into state['ShipLockerJSON']. + entry = state['ShipLockerJSON'] + odyssey_plural_microresource_types = ('Items', 'Components', 'Data', 'Consumables') # we're getting new data here. so reset it on inara's side just to be sure that we set everything right reset_data = [{'itemType': t} for t in odyssey_plural_microresource_types] From a7722f70fe8b1cd3a2096ff0ce0b524bdcae3763 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:04:38 +0100 Subject: [PATCH 80/89] No need for [] if it's a generator inside all() anyway. --- monitor.py | 2 +- plugins/inara.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monitor.py b/monitor.py index abc9d952..1bdcdfcd 100644 --- a/monitor.py +++ b/monitor.py @@ -853,7 +853,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # written to a separate shiplocker.json file - other updates will just update that file and mention it # has changed with an empty shiplocker event in the main journal. - if not all([entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')]): + if not all(entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')): logger.debug('ShipLocker event is an empty one (at least missing one data type') # So attempt to load data from the most recent file instead currentdir_path = pathlib.Path(str(self.currentdir)) diff --git a/plugins/inara.py b/plugins/inara.py index ee43a592..7c8b3fdf 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -1101,7 +1101,7 @@ def journal_entry( # noqa: C901, CCR001 elif event_name == 'ShipLocker': # In ED 4.0.0.400 the event is only full sometimes, other times indicating # ShipLocker.json was written. - if not all([entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')]): + if not all(entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')): # So it's an empty event, core EDMC should have stuffed the data # into state['ShipLockerJSON']. entry = state['ShipLockerJSON'] From 7051aba656fabcc61f8cf6a3b9357362fec63980 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:08:57 +0100 Subject: [PATCH 81/89] ShipLocker/Backpack: Use one event for the now-pass'ed ones. Also, just dropped `TransferMicroResources` entirely as it's no longer present (and would be irrelevant). --- monitor.py | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/monitor.py b/monitor.py index 1bdcdfcd..278ba01e 100644 --- a/monitor.py +++ b/monitor.py @@ -1000,36 +1000,9 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # But still record the credits balance change. self.state['Credits'] += entry.get('Price', 0) - elif event_type == 'TradeMicroResources': - # As of 4.0.0.400 we can ignore this as an empty (see file) - # `ShipLocker` event is written for the full new inventory. - pass - - elif event_type == 'TransferMicroResources': - # Defunct in 4.0.0.400 ? Not seen in testing, and we get a - # new empty/file `ShipLocker` event, along with a - # `BackpackChange` event per item type transferred. - pass - - elif event_type == 'CollectItems': - # 4.0.0.400 (still) has a BackpackChange event as well, so - # ignore this for inventory purposes. - pass - - elif event_type == 'DropItems': - # 4.0.0.400 (still) has a BackpackChange event as well, so - # ignore this for inventory purposes. - pass - - elif event_type == 'UseConsumable': - # TODO: XXX: From v31 doc - # 12.2 BackpackChange - # This is written when there is any change to the contents of the - # suit backpack – note this can be written at the same time as other - # events like UseConsumable - - # In 4.0.0.400 we do get this event, but *also* a `BackpackChange` event, - # so we ignore this for inventory purposes. + elif event_type in ('TradeMicroResources', 'CollectItems', 'DropItems', 'UseConsumable'): + # As of 4.0.0.400 we can ignore these as an empty (see file) + # `ShipLocker` event and/or a `BackpackChange` is also written. pass # TODO: From 937aaccec4cc9efb9a70d6a6cb32dad4db9ac813 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:20:27 +0100 Subject: [PATCH 82/89] ShipLockerJSON: Ensure initialised, and document it. --- PLUGINS.md | 6 ++++++ monitor.py | 1 + 2 files changed, 7 insertions(+) diff --git a/PLUGINS.md b/PLUGINS.md index a5f39edd..e7bab342 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -603,6 +603,7 @@ Content of `state` (updated to the current journal entry): | `Data` | `dict` | 'Data' MicroResources in Odyssey, `int` count each. | | `BackPack` | `dict` | `dict` of Odyssey MicroResources in backpack. | | `BackpackJSON` | `dict` | Content of Backpack.json as of last read. | +| `ShipLockerJSON` | `dict` | Content of ShipLocker.json as of last read. | | `SuitCurrent` | `dict` | CAPI-returned data of currently worn suit. NB: May be `None` if no data. | | `Suits` | `dict`[1] | CAPI-returned data of owned suits. NB: May be `None` if no data. | | `SuitLoadoutCurrent` | `dict` | CAPI-returned data of current Suit Loadout. NB: May be `None` if no data. | @@ -672,6 +673,11 @@ New in version 5.1.0: `state` entries added for Taxi, Dropship, Body and BodyType. +New in version 5.1.1: + +`state` now has a `ShipLockerJSON` member containing the un-changed, loaded, +JSON from the `ShipLockerJSON.json` file. + ##### Synthetic Events A special "StartUp" entry is sent if EDMC is started while the game is already diff --git a/monitor.py b/monitor.py index 278ba01e..c69b4996 100644 --- a/monitor.py +++ b/monitor.py @@ -157,6 +157,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below 'Data': defaultdict(int), # Backpack Data }, 'BackpackJSON': None, # Raw JSON from `Backpack.json` file, if available + 'ShipLockerJSON': None, # Raw JSON from the `ShipLocker.json` file, if available 'SuitCurrent': None, 'Suits': {}, 'SuitLoadoutCurrent': None, From 2353889ed23a868e96bc571d3294cbefd05d4f96 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:21:33 +0100 Subject: [PATCH 83/89] monitor: Make 'empty ShipLocker event' log be TRACE --- monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor.py b/monitor.py index c69b4996..a2add0c4 100644 --- a/monitor.py +++ b/monitor.py @@ -855,7 +855,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # has changed with an empty shiplocker event in the main journal. if not all(entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')): - logger.debug('ShipLocker event is an empty one (at least missing one data type') + logger.trace('ShipLocker event is an empty one (missing at least one data type)') # So attempt to load data from the most recent file instead currentdir_path = pathlib.Path(str(self.currentdir)) # Confirmed filename for 4.0.0.400 From e754568b28cd2add84c56df234266ed6afe45423 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:31:26 +0100 Subject: [PATCH 84/89] ShipLocker: Fix the all() generator --- monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor.py b/monitor.py index a2add0c4..703d23b6 100644 --- a/monitor.py +++ b/monitor.py @@ -854,7 +854,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # written to a separate shiplocker.json file - other updates will just update that file and mention it # has changed with an empty shiplocker event in the main journal. - if not all(entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')): + if not all(t in entry for t in ('Components', 'Consumables', 'Data', 'Items')): logger.trace('ShipLocker event is an empty one (missing at least one data type)') # So attempt to load data from the most recent file instead currentdir_path = pathlib.Path(str(self.currentdir)) From 107c44a2a918c3f2177c1b5a0da1611174ba00cc Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:36:50 +0100 Subject: [PATCH 85/89] Inara: Fix the all() generator for ShipLocker check --- plugins/inara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/inara.py b/plugins/inara.py index 7c8b3fdf..2e145a3c 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -1101,7 +1101,7 @@ def journal_entry( # noqa: C901, CCR001 elif event_name == 'ShipLocker': # In ED 4.0.0.400 the event is only full sometimes, other times indicating # ShipLocker.json was written. - if not all(entry.get(t, False) for t in ('Components', 'Consumables', 'Data', 'Items')): + if not all(t in entry for t in ('Components', 'Consumables', 'Data', 'Items')): # So it's an empty event, core EDMC should have stuffed the data # into state['ShipLockerJSON']. entry = state['ShipLockerJSON'] From 2c8d70f67df8cc6a0c69d5866179e3fe9119fa26 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 14:45:41 +0100 Subject: [PATCH 86/89] ShipLocker: Always load from the file, but be paranoid about it We'll log the "missing keys, file?" line at TRACE for now, until we're sure all this code is OK. --- monitor.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/monitor.py b/monitor.py index 703d23b6..2e5c522e 100644 --- a/monitor.py +++ b/monitor.py @@ -854,15 +854,25 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # written to a separate shiplocker.json file - other updates will just update that file and mention it # has changed with an empty shiplocker event in the main journal. - if not all(t in entry for t in ('Components', 'Consumables', 'Data', 'Items')): - logger.trace('ShipLocker event is an empty one (missing at least one data type)') - # So attempt to load data from the most recent file instead + # Always attempt loading of this. + # Confirmed filename for 4.0.0.400 + try: currentdir_path = pathlib.Path(str(self.currentdir)) - # Confirmed filename for 4.0.0.400 with open(currentdir_path / 'ShipLocker.json', 'rb') as h: # type: ignore entry = json.load(h, object_pairs_hook=OrderedDict) self.state['ShipLockerJSON'] = entry + except FileNotFoundError: + logger.warning('ShipLocker event but no ShipLocker.json file') + pass + + except json.JSONDecodeError as e: + logger.warning(f'ShipLocker.json failed to decode:\n{e!r}\n') + pass + + if not all(t in entry for t in ('Components', 'Consumables', 'Data', 'Items')): + logger.trace('ShipLocker event is an empty one (missing at least one data type)') + # This event has the current totals, so drop any current data self.state['Component'] = defaultdict(int) self.state['Consumable'] = defaultdict(int) From 2fd14381f2e788a002f654b488b84eb4e542c0b0 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 19:58:59 +0100 Subject: [PATCH 87/89] Release 5.1.1: appversion and changelog --- ChangeLog.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ config.py | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 77c39ec8..f203bb25 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -17,6 +17,63 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in the source (it's not distributed with the Windows installer) for the currently used version in a given branch. +Release 5.1.1 +=== + +The big change in this is adjustments to be in line with Journal changes in +Elite Dangerous Odyssey 4.0.0.400, released 2021-06-10, with respect to the +Odyssey materials Inventory. + +**This update is mandatory if you want EDMarketConnector to update Inara.cz +with your Odyssey inventory.** + +* `ShipLockerMaterials` is dead, long live `ShipLocker`. Along with other + changes to how backpack inventory is handled we should now actually be + able to fully track all Odyssey on-foot materials and consumables without + errors. + +* Inara plugin adjusted to send the new `ShipLocker` inventory to Inara.cz. + This is *still* only your *ship* inventory of Odyssey materials, not + anything currently in your backpack whilst on foot. + See [this issue](https://github.com/EDCD/EDMarketConnector/issues/1162) + for some quotes from Artie (Inara.cz developer) about *not* including + backpack contents in the Inara inventory. + +* Errors related to sending data to EDDN are now more specific to aid in + diagnoising issues. + +* Quietened some log output if we encounter connection errors trying to + utilise the Frontier CAPI service. + +Bug Fixes +--- + +* Handle where the `Backpack.json` file for a `Backpack` event is a zero length + file. Closes #1138. + +* Fixed case of 'Selection' in 'Override Beta/Normal Selection' text on + Settings > Configuration. This allows translations to work. + +Plugin Developers +--- +* We've updated [Contributing.md](./Contributing.md) including: + + 1. Re-ordered the sections to be in a more logcial and helpful order. + 1. Added a section about choosing an appropriate log level for messages. + 1. fstrings now mandatory, other than some use of `.format()` with respect to + translated strings. + +* [docs/Translations.md](./docs/Translations.md) updated about a forthcoming + change to how we can programmatically check that all translation strings + have a proper comment in 'L10n/en.template' to aid translators. + +* `state` passed to `journal_entry()` now has `ShipLockerJSON` which contains + the `json.load()`-ed data from the new 'ShipLocker.json' file. We do + attempt to always load from this file, even when the `ShipLocker` Journal + event itself contains all of the data (which it does on startup, embark and + disembark), so it *should* always be populated when plugins see any event + related to Odyssey inventory. + Release 5.1.0 === diff --git a/config.py b/config.py index 94843654..ae738796 100644 --- a/config.py +++ b/config.py @@ -33,7 +33,7 @@ appcmdname = 'EDMC' # <https://semver.org/#semantic-versioning-specification-semver> # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.1.1-beta0' +_static_appversion = '5.1.1' copyright = '© 2015-2019 Jonathan Harris, 2020-2021 EDCD' update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' From 37bee03dd8e65bb0fc4d237fe5996086249331d5 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 20:01:05 +0100 Subject: [PATCH 88/89] Changelog: Translations section "nothing should have changed, yet" --- ChangeLog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f203bb25..a4d1e5ca 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -45,6 +45,16 @@ with your Odyssey inventory.** * Quietened some log output if we encounter connection errors trying to utilise the Frontier CAPI service. +Translations +--- +We believe that nothing should be worse in this version compared to 5.1.1, +although a small tweak or two might have leaked through. + +We'll be fully addressing translations in a near-future release after we've +conclude the necessary code level work for the new system. Nothing should +change for those of you helping on OneSky, other than at most the +'comments' on each translation. They should be more useful! + Bug Fixes --- From 89edd84b5cc6f782a96811f74af7195a37a07720 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Thu, 10 Jun 2021 20:02:07 +0100 Subject: [PATCH 89/89] Changelog: Translations not updated in 5.1.1 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a4d1e5ca..7d2aa806 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -55,6 +55,9 @@ conclude the necessary code level work for the new system. Nothing should change for those of you helping on OneSky, other than at most the 'comments' on each translation. They should be more useful! +Pending that work we've specifically chosen *not* to update any +translations in this release, so they'll be the same as released in 5.1.0. + Bug Fixes ---