1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-05-25 03:17:40 +03:00

Merge branch 'stable' into releases

This commit is contained in:
Athanasius 2021-06-10 20:22:39 +01:00
commit 1c5ab10d3a
22 changed files with 1582 additions and 1043 deletions

View File

@ -66,6 +66,16 @@ 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
always_run: true
default_language_version:
python: python3.9

View File

@ -17,6 +17,76 @@ 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.
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!
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
---
* 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
===

View File

@ -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
@ -20,22 +23,39 @@ 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.
**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.
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).
## 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.
---
@ -43,15 +63,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
@ -61,13 +81,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)"
@ -76,11 +96,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,
@ -88,14 +108,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.
@ -120,35 +168,26 @@ 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).
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
`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
## Linting
We use flake8 for linting all python source.
@ -158,7 +197,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
@ -190,35 +229,17 @@ 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/)
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.
---
## Control flow
@ -265,12 +286,16 @@ 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:
@ -294,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:
@ -312,24 +339,83 @@ 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
### 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.
[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.
`logging`, and thus our `logger` instances provide functions of the
following names:
As part of our flake8 linting setup we have included a linter that warns when you use either `%` or `.format` on string
literals.
- `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:
### Docstrings
- `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.
---
## 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.
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.
---
## 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.
Lack of docstrings, or them not passing some checks, *will* cause a flake8
failure in our setup.
---
## Comments
### LANG comments for translations
When adding translations you *must*
[add a LANG comment](./docs/Translations.md#add-a-lang-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:
```
```py
# HACK $elite-version-number | $date: $description
# MAGIC $elite-version-number | $date: $description
# WORKAROUND $elite-version-number | $date: $description
@ -342,18 +428,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

View File

@ -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
@ -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']
@ -694,55 +694,57 @@ 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
def set_labels(self):
"""Set main window labels, e.g. after language change."""
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
# Multicrew role label in 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') + ':' # 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: 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')) # 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: '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: 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')) # 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: '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')) # 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: 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')) # 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 > 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')) # 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."""
@ -762,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':
@ -793,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
@ -860,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:
@ -972,12 +979,18 @@ class AppWindow(object):
companion.session.invalidate()
self.login()
except companion.ServerConnectionError as e:
logger.warning(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)
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:
@ -1004,9 +1017,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 +1037,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 +1047,7 @@ class AppWindow(object):
else:
self.cmdr['text'] = monitor.cmdr
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI
# TODO: Show something else when on_foot
if monitor.state['ShipName']:
@ -1057,7 +1070,7 @@ class AppWindow(object):
else:
self.cmdr['text'] = ''
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI
self.ship['text'] = ''
if monitor.cmdr and monitor.is_beta:
@ -1166,7 +1179,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 +1267,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
@ -1436,6 +1449,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...')
@ -1721,18 +1735,16 @@ 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.
# 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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -170,6 +170,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 +541,10 @@ class Session(object):
logger.trace('Trying...')
r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore
except requests.ConnectionError as e:
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:
logger.debug('Attempting GET', exc_info=e)
raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e

View File

@ -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.0'
_static_appversion = '5.1.1'
copyright = '© 2015-2019 Jonathan Harris, 2020-2021 EDCD'
update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'

View File

@ -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')
@ -13,10 +16,23 @@ 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:
#### 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:

View File

@ -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(

View File

@ -1,6 +1,7 @@
"""Monitor for new Journal files and contents of latest."""
import json
import pathlib
import queue
import re
import threading
@ -156,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,
@ -680,6 +682,10 @@ 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.backpack_set_empty()
elif event_type == 'Disembark':
# This event is logged when the player steps out of a ship or SRV
#
@ -842,21 +848,40 @@ 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.
# Always attempt loading of this.
# Confirmed filename for 4.0.0.400
try:
currentdir_path = pathlib.Path(str(self.currentdir))
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)
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(
@ -880,75 +905,64 @@ 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
# 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 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 None:
logger.warning('Unable to read backpack data!')
elif len(backpack_data) == 0:
logger.warning('Backpack.json was empty when we read it!')
else:
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
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
# 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)
# Assume this reflects the current state when written
self.backpack_set_empty()
clean_components = self.coalesce_cargo(entry['Components'])
self.state['BackPack']['Component'].update(
{self.canonicalise(x['Name']): x['Count'] for x in clean_components}
)
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_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_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}
)
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
@ -984,111 +998,22 @@ 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':
# 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':
# 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']
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
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']
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
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.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
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:
@ -1303,8 +1228,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.
@ -1631,6 +1555,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':
@ -1642,6 +1569,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.

View File

@ -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,
@ -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'):

View File

@ -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.
@ -590,7 +611,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'] = ''

View File

@ -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():

View File

@ -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)
@ -1097,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(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']
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]

View File

@ -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:
@ -811,15 +827,18 @@ class PreferencesDialog(tk.Toplevel):
plugdir.set(config.plugin_dir)
row = AutoInc(1)
# Section heading in settings
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(
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)
@ -827,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())
@ -837,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())
@ -877,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())
@ -885,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):
@ -1087,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:
@ -1120,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

View File

@ -15,9 +15,10 @@ 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
types-requests==0.1.9
# Code formatting tools
autopep8==1.5.7

View File

@ -0,0 +1,335 @@
"""Search all given paths recursively for localised string calls."""
import argparse
import ast
import dataclasses
import json
import pathlib
import re
import sys
from typing import Optional
# spell-checker: words dedupe deduping deduped
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, 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}'
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
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: 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:
continue
match = COMMENT_RE.match(line)
if not match:
print(line)
continue
comment = match.group(1).strip()
if not comment.startswith('# LANG:'):
bad_comment = f'Unknown comment for {file}:{current} {line}'
continue
out = comment.replace('# LANG:', '').strip()
bad_comment = None
break
if bad_comment is not None:
print(bad_comment, file=sys.stderr)
if out is None:
print(f'No comment for {file}:{current} {line}', file=sys.stderr)
return out
def scan_file(path: pathlib.Path) -> list[ast.Call]:
"""Scan a file for ast.Calls."""
data = path.read_text(encoding='utf-8')
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
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
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(encoding='utf-8').splitlines():
match = lang_re.match(line)
if not match:
continue
if match.group(1) != '!Language':
out.add(match.group(1))
return out
@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
line_end: Optional[int]
line_end_col: Optional[int]
@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
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 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
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)
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 = '''/* 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)
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()
out += f'/* {header} */\n'
out += f'{string} = {string};\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'])
group = parser.add_mutually_exclusive_group()
group.add_argument('--json', action='store_true', help='JSON 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()
directory = pathlib.Path(args.directory)
res = scan_directory(directory, [pathlib.Path(p) for p in args.ignore])
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_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)
} for (path, calls) in res.items() for c in calls
]
print(json.dumps(to_print_data, indent=2))
elif args.lang:
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():
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()

194
stats.py
View File

@ -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)

View File

@ -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

View File

@ -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'),

View File

@ -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)