diff --git a/.flake8 b/.flake8 index bdb25ca4..179e00ec 100644 --- a/.flake8 +++ b/.flake8 @@ -7,7 +7,6 @@ exclude = FDevIDs/ venv/ .venv/ - hotkey/darwin.py # FIXME: Check under macOS VM at some point # Show exactly where in a line the error happened #show-source = True diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a75b9ef8..7eb2c0f5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,22 +7,19 @@ assignees: '' --- -**Please check the [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) in case this has already been reported.** - -**Please also check if the issue is covered in our [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting).** It might be something with a known work around, or where a third party (such as EDSM) is causing logging that is harmless. - **Please complete the following information:** - - Version: [e.g. 4.0.6 - See 'Help > About E:D Market Connector'. If running from source using git then please paste the output of `git log --decorate=full | head -1`] + +[//]: # (You can gather most of this information with the EDMC System Profiler) + + - Version: [e.g. 5.10.4+39af6c34`] - Game Version: [e.g. 'Live' or 'Odyssey'] - OS: [e.g. Windows 10, Linux Debian 10.6, etc.] - OS Locale: [e.g. English, French, Serbian...] - If applicable: Browser [e.g. chrome, safari] - - Please attach **BOTH** log files, by dragging and dropping them into this input: - 1. `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file). - 1. `%TEMP%\EDMarketConnector\EDMarketConnector-debug.log`. See [Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files). NB: If you don't have this log file then you're not running the latest version of the application and should update first to see if we already fixed the bug you're reporting. **Describe the bug** -A clear and concise description of what the bug is. + +[//]: # (A clear and concise description of what the bug is.) **To Reproduce** Steps to reproduce the behavior: @@ -32,10 +29,24 @@ Steps to reproduce the behavior: 4. See error **Expected behavior** -A clear and concise description of what you expected to happen. + +[//]: # (A clear and concise description of what you expected to happen.) **Screenshots** -If applicable, add screenshots to help explain your problem. + +[//]: # (If applicable, add screenshots to help explain your problem.) **Additional context** -Add any other context about the problem here. + +**Please Confirm the Following...** + +[//]: # (Add any other context about the problem here.) +- [ ] I have checked the [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) list to ensure this is not a duplicate +- [ ] I have checked the [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting) to check for known workarounds + +**Logs** +Please attach both the EDMarketConnector.log and EDMarketConnector-debug.log if available. + +You can find these logs at `%TEMP%\EDMarketConnector.log` and `%TEMP%\EDMarketConnector\EDMarketConnector-debug.log` + +See [Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files) for information on the Debug Log files diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..a926715b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,13 @@ +# Reporting Security Issues + +EDMC takes security very seriously. Our users trust us to provide a secure and safe tool to support their experience in Elite. + +In general, the best way to report a major security issue with us that should not be publically discussed is to email our maintainer teams. + +The best point of contact for this is edmc@hullseals.space. When contacting, be sure to include as much information in your report. + +As soon as your report is processed, we'll get in touch to make sure we quickly move ahead with fixing the issue and will lay out a timeline for public disclosure and fixes. + +Another method of reporting vulnerabilities is to open a new Bug Report [here](https://github.com/EDCD/EDMarketConnector/issues/new?assignees=&labels=bug%2C+unconfirmed&projects=&template=bug_report.md&title=). + +If reporting a security issue here, do not include details as to the issue or steps to reproduce, simply indicate you have found a potential security bug and would like us to contact you directly. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..0d1c9ac1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + +# Description + + +# Example Images + + +# Type of Change + + +# How Tested + + +# Notes + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..a709c7dc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,95 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches-ignore: + - 'stable' + - 'releases' + - 'beta' + pull_request: + branches: [ develop ] + schedule: + - cron: '38 5 * * 4' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: 'ubuntu-latest' + timeout-minutes: 360 + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ea9a3093..17841159 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -13,13 +13,13 @@ on: jobs: code-checks: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: # Debug show the info we have to work with - name: Show github context - run: cat $GITHUB_EVENT_PATH + run: cat "$GITHUB_EVENT_PATH" #################################################################### # Checkout the necessary commits @@ -44,22 +44,23 @@ jobs: echo "BASE_REPO_URL: ${BASE_REPO_URL}" echo "BASE_REPO_OWNER: ${BASE_REPO_OWNER}" # Add the 'base' repo as a new remote - git remote add ${BASE_REPO_OWNER} ${BASE_REPO_URL} + git remote add "${BASE_REPO_OWNER}" "${BASE_REPO_URL}" # And then fetch its references - git fetch ${BASE_REPO_OWNER} + git fetch "${BASE_REPO_OWNER}" #################################################################### #################################################################### # Get Python set up #################################################################### - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version-file: '.python-version' + cache: 'pip' # caching pip dependencies - name: Install dependencies run: | python -m pip install --upgrade pip - pip install wheel flake8 + pip install flake8 if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; elif [ -f requirements.txt ]; then pip install -r requirements.txt ; fi #################################################################### diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index 803bfe95..3e83fbfb 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -19,20 +19,21 @@ on: jobs: push_checks: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: + cache: 'pip' # caching pip dependencies python-version-file: '.python-version' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install wheel flake8 pytest + pip install flake8 pytest if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Setup flake8 annotations @@ -42,7 +43,7 @@ jobs: env: ROOT_SHA: ${{github.base_ref}} run: | - DATA=$(jq --raw-output .before $GITHUB_EVENT_PATH) + DATA=$(jq --raw-output .before "$GITHUB_EVENT_PATH") echo "DATA: ${DATA}" ####################################################################### @@ -68,3 +69,7 @@ jobs: - name: mypy type checks run: | ./scripts/mypy-all.sh --platform win32 + + - name: translation checks + run: | + python ./scripts/find_localised_strings.py --compare-lang L10n/en.template --directory . --ignore coriolis-data diff --git a/.github/workflows/submodule-update.yml b/.github/workflows/submodule-update.yml index f794bdeb..74213c29 100644 --- a/.github/workflows/submodule-update.yml +++ b/.github/workflows/submodule-update.yml @@ -54,7 +54,7 @@ jobs: - name: Create submodules changes branch if: steps.check_for_changes.outputs.changes == 'true' run: | - git checkout -b "submodule-change/$GITHUB_RUN_ID" $CHECKOUT_BRANCH + git checkout -b "submodule-change/$GITHUB_RUN_ID" "$CHECKOUT_BRANCH" git commit -am "updating submodules" git push --set-upstream origin "submodule-change/$GITHUB_RUN_ID" diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index a3de381b..8a4d115a 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -1,5 +1,5 @@ # vim: tabstop=2 shiftwidth=2 -name: Build EDMC for Windows +name: Build EDMC on: push: @@ -11,7 +11,7 @@ jobs: variables: outputs: sem_ver: ${{ steps.var.outputs.sem_ver }} - archive_exclusions: ${{ steps.var.outputs.archive_exclusions }} + short_sha: ${{ steps.var.outputs.short_sha }} runs-on: "ubuntu-latest" steps: - name: Setting global variables @@ -20,6 +20,7 @@ jobs: with: script: | core.setOutput('sem_ver', '${{ github.ref_name }}'.replaceAll('Release\/', '')) + core.setOutput('short_sha', '${{ github.sha }}'.substring(0, 8)) linux_build: needs: [variables] @@ -29,18 +30,25 @@ jobs: - uses: actions/checkout@v4 with: submodules: true + - name: Create .gitversion + run: | + echo "${{ needs.variables.outputs.short_sha }}" > .gitversion - name: Make tar archive run: | # For 'tar' we can only specify filename/glob exclusions, not any # directory location tar -c -v -z \ - -f ../EDMarketConnector-release-${{ needs.variables.outputs.sem_ver }}.tar.gz \ + -f "../EDMarketConnector-release-${{ needs.variables.outputs.sem_ver }}.tar.gz" \ -C .. \ --exclude=EDMarketConnector-release-*.* \ --exclude=.editorconfig \ --exclude=.flake8 \ - --exclude=.git* \ + --exclude=.gitattributes \ + --exclude=.gitignore \ + --exclude=.gitmodules \ + --exclude=.git \ + --exclude=.github \ --exclude=.mypy.ini \ --exclude=.pre-commit-config.yaml \ --exclude=build.py \ @@ -51,19 +59,19 @@ jobs: --exclude=scripts \ --exclude=tests \ EDMarketConnector - mv ../EDMarketConnector-release-${{ needs.variables.outputs.sem_ver }}.tar.gz . + mv "../EDMarketConnector-release-${{ needs.variables.outputs.sem_ver }}.tar.gz" . - name: Upload build files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: Built files + name: built-files-linux path: | EDMarketConnector-release-*.tar.gz windows_build: needs: [variables] name: Build EDMC - runs-on: windows-2019 + runs-on: windows-latest defaults: run: @@ -93,21 +101,21 @@ jobs: # presumably due to a Windows CL length limit. exclusions: 'EDMarketConnector/EDMarketConnector-release-*.* EDMarketConnector/.editorconfig EDMarketConnector/.flake8 EDMarketConnector/.git* EDMarketConnector/.mypy.ini EDMarketConnector/.pre-commit-config.yaml EDMarketConnector/build.py EDMarketConnector/*.manifest EDMarketConnector/coriolis-data/ EDMarketConnector/img/ EDMarketConnector/pyproject.toml EDMarketConnector/scripts/ EDMarketConnector/tests/' - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version-file: '.python-version' architecture: "x86" + cache: 'pip' # caching pip dependencies - name: Install python tools run: | - pip install wheel pip install -r requirements-dev.txt - name: Download winsparkle run: | - Invoke-Webrequest -UseBasicParsing https://github.com/vslavik/winsparkle/releases/download/v0.8.0/WinSparkle-0.8.0.zip -OutFile out.zip + Invoke-Webrequest -UseBasicParsing https://github.com/vslavik/winsparkle/releases/download/v0.8.1/WinSparkle-0.8.1.zip -OutFile out.zip Expand-Archive out.zip - Move-Item 'out\WinSparkle-0.8.0\Release\*' '.\' + Move-Item 'out\WinSparkle-0.8.1\Release\*' '.\' - name: Build EDMC run: | @@ -118,23 +126,36 @@ jobs: with: filepath: './EDMC_Installer_Config.iss' + - name: Rename Installer + run: | + Get-ChildItem -Path . -Filter "EDMarketConnector_Installer_*.exe" | Rename-Item -NewName {"EDMarketConnector_Installer_Unsigned_$($_.Name -replace '^EDMarketConnector_Installer_', '')"} + - name: Upload build files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: Built files + name: built-files-windows path: | EDMarketConnector_Installer_*.exe EDMarketConnector-release-*.zip + Merge: + runs-on: ubuntu-latest + needs: [ windows_build, linux_build ] + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: Built files + pattern: built-files-* release: name: Release new version runs-on: ubuntu-latest - needs: [ windows_build, linux_build ] + needs: Merge if: "${{ github.event_name != 'workflow_dispatch' }}" steps: - name: Download binary - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: Built files path: ./ @@ -143,7 +164,7 @@ jobs: run: sha256sum EDMarketConnector_Installer_*.exe EDMarketConnector-release-*.{zip,tar.gz} > ./hashes.sum - name: Create Draft Release - uses: "softprops/action-gh-release@v1" + uses: "softprops/action-gh-release@v2" with: token: "${{secrets.GITHUB_TOKEN}}" draft: true diff --git a/.gitignore b/.gitignore index 4283fcb0..1df2f666 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,9 @@ build dist.win32/ dist.* -# Ignore generated ChangeLog.html file +# Ignore generated ChangeLog files ChangeLog.html +/scripts/script_output # Ignore files dump @@ -30,6 +31,8 @@ appcast_mac_*.xml EDMC_Installer_Config.iss EDMarketConnector.wxs wix/components.wxs +edmarketconnector.xml +edmarketconnector-beta.xml # Ignore Visual Elements Manifest file for Windows EDMarketConnector.VisualElementsManifest.xml @@ -52,6 +55,8 @@ htmlcov/ .coverage pylintrc pylint.txt +.pylintrc # Ignore Submodule data directory coriolis-data/ +FDevIDs/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index cd89b465..c65e62f1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "coriolis-data"] path = coriolis-data url = https://github.com/EDCD/coriolis-data.git -[submodule "FDevIDs"] - path = FDevIDs - url = https://github.com/EDCD/FDevIDs.git diff --git a/.mypy.ini b/.mypy.ini index 75f4ccdb..9ac2f227 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -6,5 +6,4 @@ scripts_are_modules = True ; ` = ` ; i.e. no typing info. check_untyped_defs = True -; platform = darwin explicit_package_bases = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b534279..51702e14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - id: mypy # verbose: true # log_file: 'pre-commit_mypy.log' - additional_dependencies: [ types-pkg-resources, types-requests, types-urllib3 ] + additional_dependencies: [ types-setuptools, types-requests, types-urllib3 ] # args: [ "--follow-imports", "skip", "--ignore-missing-imports", "--scripts-are-modules" ] ### # pydocstyle.exe diff --git a/.python-version b/.python-version index 371cfe35..2419ad5b 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.1 +3.11.9 diff --git a/ChangeLog.md b/ChangeLog.md index 719c6b16..2d7c9423 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,38 +1,389 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first). --- +* We currently test against, and package with, Python 3.11.9, 32-bit. + * As a result, we do not support Windows 7, 8, or 8.1. + * Developers can check the contents of the `.python-version` file + in the source (not distributed with the Windows installer) for the + currently used version. +--- -Copyrights +Release 5.12.2 === -Please see the [docs/Licenses](docs/Licenses/) directory for copies of any -licenses for software we use with EDMarketConnector, either at runtime, or to -produce the Windows executables and installer. +This is a pre-release to test a number of bugfixes and security improvements in EDMC. ---- +**Changes and Enhancements** +* Adds a guard against workflow shell execution vulnerabilities in GitHub Actions +* Adds a "Copy" icon in the EDMC System Profiler +* Includes additional Loadout event properties in the "State" context +* Updates Dependencies and Submodules +* Removes an outdated development script that was no longer in use and prevented dependency updates +* Replaces types-pkg-resources with types-setuptools per PyPi documentation -* We now test against, and package with, Python 3.11.1, 32-bit. +**Bug Fixes** +* Removes Duplicate Coriolis Definitions Included In Submodules +* Adds Context Support for Mandalay and Cobra Mk V, and Type-8 Transporter +* Adds a number of missing modules to modules.json +* Fixes a widely-reported bug where missing HullValue or ModuleValue entries would cause parsing to crash +* Fixes a bug where PSUtils exception handling was not processed - **As a consequence of this we no longer support Windows 7. - This is due to - [Python 3.10.x and later not supporting Windows 7](https://www.python.org/downloads/windows/). - The application (both EDMarketConnector.exe and EDMC.exe) will crash on - startup due to a missing DLL.** +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. - As [Windows 8.1 is now End-Of-Life](https://learn.microsoft.com/en-us/lifecycle/faq/windows#windows-8-1) - we no longer explicitly support it, but for the time being it will likely - continue to work. This is dependent on future Python releases not dropping - support for Windows 8.1 in a manner that prevents it working. Any bug report - made against Windows 8.1 use may be ignored unless reproduced on a supported - OS. +Release 5.12.1 +=== - This should have no other impact on users or plugin developers, other - than the latter now being free to use features that were introduced since the - Python 3.7 series. +This release fixes a handful of bugs reported with 5.12.0, notably a widely-reported bug with EDMC CAPI Authentication. - Developers can check the contents of the `.python-version` file - in the source (it's not distributed with the Windows installer) for the - currently used version in a given branch. +**Changes and Enhancements** +* Fixed a typo in the prior release notes + +**Bug Fixes** +* Fixed a bug where the EDMC System Profiler wouldn't load details properly +* Reverted a number of usages of Pathlib back to os.path for further validation testing +* Fixed a bug where EDMC would error out with a max() ValueError +* Fixed an issue where the EDMC protocol wouldn't be processed properly via prototyping + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. + + +Release 5.12.0 +=== + +This release brings a number of performance enhancements and functionality updates requested by the community to EDMC. +Notably, integration with Inara's SLEF notation, custom plugin directories, streamlined logging locations, and +performance enhancements are included. + +This release also fixes a few administrative issues regarding licenses to ensure compliance with included libraries. + +**Changes and Enhancements** +* Added the ability to export a ship's loadout to Inara SLEF notation +* Added the ability for EDMC to restart itself if required after settings changes +* Added the ability to change the custom plugins directory to allow for multiple plugin profiles +* Added Basic Type 8 Support +* Updated the default logging directory from $TEMPDIR or %TEMP% and to the current app data directory +* Updated a number of direct win32API calls to use proper prototyped library calls +* Updated a number of translations +* Updated a number of dependencies +* Updated included and bundled licenses to comply with dependency requirements +* Updated the game_running check to be more efficient on Windows to reduce program hangs +* Minor logic enhancements +* Retired most usages of os.path in favor of the preferred PathLib + +**Bug Fixes** +* Fixed a bug that would result in Horizons and Odyssey flags not being passed to EDDN + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. + + +Release 5.11.3 +=== + +This release fixes a bug where an incomplete hand-over from ordereddict to dict types would cause a sender failure. + +**Changes and Enhancements** +* Updated Translations + +**Bug Fixes** +* Fixed a bug where two senders might fail due to improper data formats + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. +* FDevID files (`commodity.csv` and `rare_commodity.csv`) have moved their preferred location to the app dir (same location as default Plugins folder). Please migrate to use `config.app_dir_path`. + +Release 5.11.2 +=== + +This release fixes a bug where minimizing to the system tray could cause the program to not un-minimize. + +**Changes and Enhancements** +* Updated Translations +* Added a developer utility to help speed up changelog development + +**Bug Fixes** +* Fixed a bug where minimizing to the system tray could cause the program to not un-minimize. + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. +* FDevID files (`commodity.csv` and `rare_commodity.csv`) have moved their preferred location to the app dir (same location as default Plugins folder). Please migrate to use `config.app_dir_path`. + +Release 5.11.1 +=== + +This release fixes a bug regarding FDevID files when running from Source in a non-writable location. Additionally, +Deprecation Warnings are now more visible to aid in plugin development. + +**Changes and Enhancements** +* Added a check on Git Pushes to check for updated translation strings for developers +* Enabled deprecation warnings to pass to plugins and logs +* Updated Dependencies +* Replaced infi.systray with drop-in replacement simplesystray + +**Bug Fixes** +* Fixed a bug that could result in the program not updating or writing FDevID files when running from source in a location where the running user can't write to + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. +* FDevID files (`commodity.csv` and `rare_commodity.csv`) have moved their preferred location to the app dir (same location as default Plugins folder). Please migrate to use `config.app_dir_path`. + + +Release 5.11.0 +=== + +This release includes a number of new features and improvements, including a new Beta Update Track for testing future updates, enhanced context menus for text entry fields and UI elements, a revamp to the existing translation system and logging capabilities, and more. This release includes the Python Image Library (PIL) into our core bundle, adds a number of stability and configuration checks to the tool, and adds new schemas and configuration values to senders. + +This release also includes a number of bug fixes, performance enhancements, and updates to various aspects of the code to enhance maintainability are included. Notably, MacOS support has been removed due to a lack of support for this OS in Elite, and a number of functions have been deprecated and will be removed in later versions. Plugin developers, take note! + +**Changes and Enhancements** +* Established a Beta Update Track to allow users to assist in future update testing +* Added a global context menu for text entry fields that includes cut, copy, and paste options +* Added a context menu for Ship, System, and Station UI elements which allows opening the respective link in any of the available resource providers. +* Added translation hooks to the update available status string +* Added additional status logging when we're awaiting game log-in +* Added the Python Image Library (PIL) to the core EDMC library bundle +* Added respect for EDSM API limits to the default plugin +* Added EDDN stationType and carrierDockingAccess schemas to the sent events +* Added MaxJumpRange and CargoCapacity events to the Inara sender +* Added a high-level critical error handler to gracefully terminate the program in the event of a catastrophic error +* Added the ability to override the default language for a translation by adding the optional 'lang' parameter to the translate function for individual functions +* Added an updated template and new security reporting guidance to the documentation +* Added a new updater for the FDevID Files to keep the dependency up to date without requiring a new patch version push +* Added a System Profiler Utility to assist with gathering system and environment information for bug report purposes +* Added a new security policy for responsible disclosure of identified security issues +* Adds Additional Error Processing to the System Profiler when launched from EDMC +* Adds the ability to resize the Settings window to larger than the initial default size +* Enabled security code scanning on the GitHub repository +* Tweaked a few list length checks that could just be boolean to be bool +* Updates the look and feel of the "Already Running" popup to reduce overhead and improve the look of the popup +* Updated translations to latest versions, including a new language: Ukranian! +* Updated documentation to reflect certain changes to the code +* Updated the GitHub Bug Report template +* Updated the GitHub Pull Request template +* Updated internal workflows to more recent versions +* Updated util_ships to avoid using Windows reserved file names as output +* Converted all usages of the unnecessary OrderedDict to use the standard dict +* Clarifies the hierarchy of parent classes for custom MyNotebook classes +* Renamed the default translation function from `_()` to `tr.tl()` +* Renamed the Translations base class to conform to Pythonic standards +* Deprecated the `_Translations` class +* Deprecated the `Translations` singleton in favor of `translations` +* Unpinned several dependencies that were already dependencies of other dependencies to prevent dependency conflicts (say that 5 times fast) +* Updated a few type hints to allow updates to more updated dependencies +* Changed the translation function import to no longer rely on forcing it into Python's builtins +* Handed over a few tk classes to their ttk equivalents for better styling +* Reworked the Plugin system to no longer use the deprecated importlib.load_module() +* Deprecated nb.Entry and nb.ColoredButton as they simply point toward other classes with no processing +* Removed macOS support +* Removed deprecated modules.p and ships.p files +* Removed deprecated openurl() function + +**Bug Fixes** +* Fixed a bug where certain types of exceptions from the Requests module wouldn't be handled properly regarding killswitches +* Fixed a rare bug where source builds running on 64-bit Python could generate an OverflowError in the monitor system +* Fixed a bug where EDMC would open directories in the webbrowser instead of the file explorer on Linux +* Fixed a rare bug that could cause the EDSM plugin to crash due to missing configuration values + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. +* modules.p and ships.p are deprecated, and have been removed +* The `openurl()` function in ttkHyperlinkLabel has been removed. Please migrate to `webbrowser.open()` + + +Release 5.10.6 +=== +This release contains the data information for the new SCO modules added in Elite update 18.04. +This should represent full support for the new Python Mk II. + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Added new SCO Module Details +* Reverted a change from the prior release due to breaking some consumers. +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + +Release 5.10.5 +=== +This release contains a fix for a bug that could crash EDMC's console versions when reading outfitting information +from the new SCO Frame Shift Drive modules. + +Please note that this does not offer full support for the new SCO modules or the Python Mk II. More support will +be added in a future update. + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Updated Translations +* Added limited data regarding the Python Mk II +* Added a few Coriolis module information entries + +**Bug Fixes** +* Fixed a bug that could cause the new SCO modules to display improper ratings or sizes +* Fixed a bug where the new SCO modules would display as a normal Frame Shift Drive +* Fixed a bug which could crash EDMC if the exact details of a Frame Shift Drive were unknown + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + +Release 5.10.4 +=== +This release contains updated dependencies, modules files, translations, and adds two new EDDN schemas. It also +adds Turkish translations to EDMC! + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Adds Turkish Translations to EDMC +* Adds DockingDenied and DockingGranted EDDN Schemas +* Updated FDevIDs Dependency +* Updated Translations +* Updated modules files to process several missing module types used for bug squishing or going fast +* Updated Python Dependencies + +**Bug Fixes** +* Fixed a bug on older Python versions which couldn't import updated type annotations + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated for removal in 5.11+! +* The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in 5.11+! Please migrate to `webbrowser.open()`. + +Release 5.10.3 +=== +This release contains a bugfix for the shipyard outfitting parsing system and an update to the French translations. + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Updated French Translations + +**Bug Fixes** +* Fixed a bug that crashed the outfitting system when encountering armor. (Thanks TCE team for identifying this one!) + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated +for removal in the next major release! Please look for that change coming soon. +* Note to plugin developers: The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in the next major release! Please migrate to `webbrowser.open()`. + +Release 5.10.2 +=== +This release contains updated dependencies, some bug fixes, a few minor enhancements to some supporting files, +and some resorted resources as well as a new image for some of the built EXEs. + +We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines. +For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC + +**Changes and Enhancements** +* Added additional logging to the Python build string in the case of missing files +* Added a new icon to EDMC's Command-Line EXE +* Added additional logging to the build system +* Updated several dependencies +* Updated FDEV IDs +* Updated relevant copyright dates +* Updated automatic build script to support code signing workflow +* Updated translations to the latest versions +* Moved a few unused files to the resources folder. These files have no references in the code + +**Bug Fixes** +* Fixed a bug that could cause EDMC to handle SIGINT signals improperly +* Fixed a bug that could result in URL providers to be set to invalid values +* Fixed a bug that could result in Coriolis URL providers to revert back to "Auto" on language translations +* Fixed a bug where Inara didn't understand being blown up by a Thargoid, and blew itself up instead +* Fixed a printing issue for the localization system for unused strings + +**Removed Files** +* Removed two unused manifest and MacOS icon files which are no longer in use. + +**Known Issues** +* Some users of TCE have reported issues with newer versions of EDMC with TCE. + * We have been unable to replicate this issue. If you are able to assist, please + add your information here: https://github.com/EDCD/EDMarketConnector/issues/2176 + +**Plugin Developers** +* modules.p and ships.p are deprecated, and slated +for removal in the next major release! Please look for that change coming soon. +* Note to plugin developers: The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in the next major release! Please migrate to `webbrowser.open()`. + +Release 5.10.1 +=== +This release contains a number of bugfixes, minor performance enhancements, +workflow and dependency updates, and a function deprecation. + +Note to plugin developers: modules.p and ships.p are deprecated, and slated +for removal in the next major release! Please look for that change coming soon. + +Note to plugin developers: The `openurl()` function in ttkHyperlinkLabel has been deprecated, +and slated for removal in the next major release! Please migrate to `webbrowser.open()`. + +**Changes and Enhancements** +* Deprecated `openurl()`. Please migrate to `webbrowser.open()` +* Updated a number of list comparisons to use more efficient tuple comparisons +* Updated a few type hints +* Updated a few binary comparitors to be more efficient +* Moved `resources.json` and `modules.json` back to the top level for all users +* Updated several dependencies +* Updated Python version to 3.11.7 + +**Bug Fixes** +* Fixed an issue where resources files could be in different locations for different users. + * These files are now in the same location (top level) for all users on all distributions. +* Fixed an issue where CMDRs without the Git application installed would crash on start if running from Source. + * Thanks to the Flatpak team for pointing this one out! +* Fixed a bug where CMDRs running from source would have their git hash version displayed as UNKNOWN. + * We're now more failure tolerant and use the bundled .gitversion if no true git hash is provided. +* Fixed a bug where starting two copies of EDMC with a valid install would not generate a duplicate warning. ---- Release 5.10.0 === This release contains a number of under-the-hood changes to EDMC designed to improve performance, code @@ -43,7 +394,7 @@ for removal in the next major release! Please look for that change coming soon. **Changes and Enhancements** * Added new `modules.json` and `ships.json` files to improve security and readability -* Added a core Spanch URL provider plugin +* Added a core Spansh URL provider plugin * Added a new auth response page for successful FDEV authentication * Added a new Open Log Folder option to the Help menu * Added a new `--start_min` command flag to force the application to start minimized @@ -109,7 +460,7 @@ Known Issues Release 5.9.3 === -This release is identical to 5.9.3, except reverts a bad change. +This release is identical to 5.9.2, except reverts a bad change. - REVERTS Deprecated load_module() is now retired (#1462) diff --git a/Contributing.md b/Contributing.md index 5f7bc158..f021f1a3 100644 --- a/Contributing.md +++ b/Contributing.md @@ -679,7 +679,7 @@ the following does not work: ```py from sys import platform -if platform == 'darwin': +if platform == 'win32': ... ``` diff --git a/EDMC.py b/EDMC.py index 072b0492..6b72c7d1 100755 --- a/EDMC.py +++ b/EDMC.py @@ -34,7 +34,7 @@ import collate import commodity import companion import edshipyard -import l10n +from l10n import translations as tr import loadout import outfitting import shipyard @@ -42,7 +42,7 @@ import stats from commodity import COMMODITY_DEFAULT from config import appcmdname, appversion, config from monitor import monitor -from update import EDMCVersion, Updater +from update import EDMCVersion, Updater, check_for_fdev_updates sys.path.append(config.internal_plugin_dir) # This import must be after the sys.path.append. @@ -65,7 +65,7 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' ) -l10n.Translations.install_dummy() +tr.install_dummy() SERVER_RETRY = 5 # retry pause for Companion servers [s] EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS, \ @@ -162,7 +162,9 @@ def main(): # noqa: C901, CCR001 updater = Updater() newversion: EDMCVersion | None = updater.check_appcast() if newversion: - print(f'{appversion()} ({newversion.title!r} is available)') + # LANG: Update Available Text + newverstr: str = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title) + print(f'{appversion()} ({newverstr})') else: print(appversion()) return @@ -495,6 +497,10 @@ def main(): # noqa: C901, CCR001 if __name__ == '__main__': - main() + try: + check_for_fdev_updates(silent=True) + main() + except KeyboardInterrupt: + logger.info("Ctrl+C Detected, Attempting Clean Shutdown") logger.debug('Exiting') sys.exit(EXIT_SUCCESS) diff --git a/EDMCLogging.py b/EDMCLogging.py index 74b439f9..49b1aa40 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -26,12 +26,13 @@ To utilise logging in core code, or internal plugins, include this: To utilise logging in a 'found' (third-party) plugin, include this: - import os + from pathlib import Path import logging - plugin_name = os.path.basename(os.path.dirname(__file__)) + # Retrieve the name of the plugin folder + plugin_name = Path(__file__).resolve().parent.name + # Set up logger with hierarchical name including appname and plugin_name # plugin_name here *must* be the name of the folder the plugin resides in - # See, plug.py:load_plugins() logger = logging.getLogger(f'{appname}.{plugin_name}') """ from __future__ import annotations @@ -41,7 +42,7 @@ import logging import logging.handlers import os import pathlib -import tempfile +import warnings from contextlib import suppress from fnmatch import fnmatch # So that any warning about accessing a protected member is only in one place. @@ -50,9 +51,7 @@ from threading import get_native_id as thread_native_id from time import gmtime from traceback import print_exc from typing import TYPE_CHECKING, cast - -import config as config_mod -from config import appcmdname, appname, config +from config import appcmdname, appname, config, trace_on # TODO: Tests: # @@ -99,9 +98,11 @@ logging.Logger.trace = lambda self, message, *args, **kwargs: self._log( # type # MAGIC-CONT: See MAGIC tagged comment in Logger.__init__() logging.Formatter.converter = gmtime +warnings.simplefilter('default', DeprecationWarning) + def _trace_if(self: logging.Logger, condition: str, message: str, *args, **kwargs) -> None: - if any(fnmatch(condition, p) for p in config_mod.trace_on): + if any(fnmatch(condition, p) for p in trace_on): self._log(logging.TRACE, message, args, **kwargs) # type: ignore # we added it return @@ -181,8 +182,7 @@ class Logger: # We want the files in %TEMP%\{appname}\ as {logger_name}-debug.log and # rotated versions. # This is {logger_name} so that EDMC.py logs to a different file. - logfile_rotating = pathlib.Path(tempfile.gettempdir()) - logfile_rotating /= f'{appname}' + logfile_rotating = pathlib.Path(config.app_dir_path / 'logs') logfile_rotating.mkdir(exist_ok=True) logfile_rotating /= f'{logger_name}-debug.log' @@ -486,8 +486,8 @@ class EDMCContextFilter(logging.Filter): :return: The munged module_name. """ file_name = pathlib.Path(frame_info.filename).expanduser() - plugin_dir = pathlib.Path(config.plugin_dir_path).expanduser() - internal_plugin_dir = pathlib.Path(config.internal_plugin_dir_path).expanduser() + plugin_dir = config.plugin_dir_path.expanduser() + internal_plugin_dir = config.internal_plugin_dir_path.expanduser() # Find the first parent called 'plugins' plugin_top = file_name while plugin_top and plugin_top.name != '': diff --git a/EDMCSystemProfiler.py b/EDMCSystemProfiler.py new file mode 100644 index 00000000..873282b8 --- /dev/null +++ b/EDMCSystemProfiler.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +EDMCSystemProfiler.py - GUI or Command-Line Tool to Print Diagnostic Information about EDMC. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" +import argparse +import locale +import webbrowser +import platform +import sys +from os import chdir, environ, path +import pathlib +import logging +from journal_lock import JournalLock + +if getattr(sys, "frozen", False): + # Under py2exe sys.path[0] is the executable name + if sys.platform == "win32": + chdir(path.dirname(sys.path[0])) + # Allow executable to be invoked from any cwd + environ["TCL_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tcl") + environ["TK_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tk") + +else: + # We still want to *try* to have CWD be where the main script is, even if + # not frozen. + chdir(pathlib.Path(__file__).parent) + +import config +from config import appversion, appname +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox +from monitor import monitor +from EDMCLogging import get_main_logger + + +def get_sys_report(config: config.AbstractConfig) -> str: + """Gather system information about Elite, the Host Computer, and EDMC.""" + # Calculate Requested Information + plt = platform.uname() + locale.setlocale(locale.LC_ALL, "") + lcl = locale.getlocale() + monitor.currentdir = config.get_str( + "journaldir", default=config.default_journal_dir + ) + if not monitor.currentdir: + monitor.currentdir = config.default_journal_dir + try: + logfile = monitor.journal_newest_filename(monitor.currentdir) + if logfile is None: + raise ValueError("None from monitor.journal_newest_filename") + + with open(logfile, "rb", 0) as loghandle: + for line in loghandle: + try: + monitor.parse_entry(line) + except Exception as e: + exception_type = e.__class__.__name__ + monitor.state["GameVersion"] = ( + exception_type + if not monitor.state["GameVersion"] + else monitor.state["GameVersion"] + ) + monitor.state["GameBuild"] = ( + exception_type + if not monitor.state["GameBuild"] + else monitor.state["GameBuild"] + ) + monitor.state["Odyssey"] = ( + exception_type + if not monitor.state["Odyssey"] + else monitor.state["Odyssey"] + ) + except Exception as e: + exception_type = e.__class__.__name__ + monitor.state["GameVersion"] = exception_type + monitor.state["GameBuild"] = exception_type + monitor.state["Odyssey"] = exception_type + + journal_lock = JournalLock() + lockable = journal_lock.open_journal_dir_lockfile() + + report = f"EDMC Version: \n - {appversion()}\n\n" + report += "OS Details:\n" + report += f"- Operating System: {plt.system} {plt.release}\n" + report += f"- Version: {plt.version}\n" + report += f"- Machine: {plt.machine}\n" + report += f"- Python Version: {platform.python_version()}\n" + report += "\nEnvironment Details\n" + report += f"- Detected Locale: {lcl[0]}\n" + report += f"- Detected Encoding: {lcl[1]}\n" + report += f"- Journal Directory: {monitor.currentdir}\n" + report += f"- Game Version: {monitor.state['GameVersion']}\n" + report += f"- Game Build: {monitor.state['GameBuild']}\n" + report += f"- Using Odyssey: {monitor.state['Odyssey']}\n" + report += f"- Journal Dir Lockable: {lockable}\n" + return report + + +def copy_sys_report(root: tk.Tk, report: str) -> None: + """Copy the system info to the keyboard.""" + root.clipboard_clear() + root.clipboard_append(report) + messagebox.showinfo("System Profiler", "System Report copied to Clipboard", parent=root) + + +def main() -> None: + """Entry Point for the System Profiler.""" + # Now Let's Begin + root: tk.Tk = tk.Tk() + root.withdraw() # Hide the window initially to calculate the dimensions + try: + icon_image = tk.PhotoImage( + file=path.join(cur_config.respath_path, "io.edcd.EDMarketConnector.png") + ) + + root.iconphoto(True, icon_image) + except tk.TclError: + root.iconbitmap(path.join(cur_config.respath_path, "EDMarketConnector.ico")) + + sys_report = get_sys_report(cur_config) + + # Set up styling + style = ttk.Style(root) + style.configure("Title.TLabel", font=("Helvetica", 10, "bold"), foreground="#333") + style.configure("Subtitle.TLabel", font=("Helvetica", 8), foreground="#555") + style.configure("Details.TLabel", font=("Helvetica", 8), foreground="#222") + + # Build UI + title_lbl = ttk.Label( + root, text="EDMarketConnector System Profiler", style="Title.TLabel" + ) + title_lbl.grid(row=0, column=0, padx=20, pady=10) + + system_details_lbl = ttk.Label( + root, text="System Details:", style="Subtitle.TLabel" + ) + system_details_lbl.grid(row=1, column=0, padx=20, pady=0, sticky="w") + + details_lbl = ttk.Label( + root, text=sys_report, style="Details.TLabel", justify="left" + ) + details_lbl.grid(row=2, column=0, padx=20, pady=5, sticky="w") + + # Buttons + sys_report_btn = ttk.Button( + root, + text="Copy System Report \U0001F5D0", + command=lambda: copy_sys_report(root, sys_report), + ) + sys_report_btn.grid(row=3, column=0, padx=20, pady=10, sticky="w") + + github_btn = ttk.Button( + root, + text="Open GitHub Bug Report", + command=lambda: webbrowser.open( + "https://github.com/EDCD/EDMarketConnector/issues/new?assignees=" + "&labels=bug%2C+unconfirmed&projects=&template=bug_report.md&title=" + ), + ) + github_btn.grid(row=3, column=0, padx=20, pady=10, sticky="e") + + # Update and get window dimensions + root.update() + width = root.winfo_reqwidth() + 20 + height = root.winfo_reqheight() + 20 + + # Set window size and show + root.geometry(f"{width}x{height}") + root.title("EDMarketConnector") + root.deiconify() + root.resizable(False, False) + + root.mainloop() + + +if __name__ == "__main__": + # Args: Only work if not frozen + parser = argparse.ArgumentParser( + prog=appname, + description="Prints diagnostic and debugging information about the current EDMC configuration.", + ) + parser.add_argument( + "--out-console", + help="write the system information to the console", + action="store_true", + ) + args = parser.parse_args() + + # Suppress Logger + logger = get_main_logger() + logger.setLevel(logging.CRITICAL) + if getattr(sys, "frozen", False): + sys.stderr._error = "inhibit log creation" # type: ignore + + cur_config = config.get_config() + if args.out_console: + sys_report = get_sys_report(cur_config) + print(sys_report) + sys.exit(0) + + main() diff --git a/EDMarketConnector.VisualElementsManifest.xml b/EDMarketConnector.VisualElementsManifest.xml deleted file mode 100644 index f0088658..00000000 --- a/EDMarketConnector.VisualElementsManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/EDMarketConnector.icns b/EDMarketConnector.icns deleted file mode 100644 index bdc87423..00000000 Binary files a/EDMarketConnector.icns and /dev/null differ diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 87ba6814..80a958e1 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -11,14 +11,16 @@ from __future__ import annotations import argparse import html import locale +import os import pathlib import queue import re +import signal import subprocess import sys import threading import webbrowser -from os import chdir, environ, path +from os import chdir, environ from time import localtime, strftime, time from typing import TYPE_CHECKING, Any, Literal from constants import applongname, appname, protocolhandler_redirect @@ -29,28 +31,29 @@ from constants import applongname, appname, protocolhandler_redirect if getattr(sys, 'frozen', False): # Under py2exe sys.path[0] is the executable name if sys.platform == 'win32': - chdir(path.dirname(sys.path[0])) + os.chdir(pathlib.Path(sys.path[0]).parent) # Allow executable to be invoked from any cwd - environ['TCL_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tcl') - environ['TK_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tk') + environ['TCL_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tcl') + environ['TK_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tk') else: # We still want to *try* to have CWD be where the main script is, even if # not frozen. chdir(pathlib.Path(__file__).parent) - # config will now cause an appname logger to be set up, so we need the # console redirect before this if __name__ == '__main__': # Keep this as the very first code run to be as sure as possible of no # output until after this redirect is done, if needed. if getattr(sys, 'frozen', False): + from config import config # By default py2exe tries to write log to dirname(sys.executable) which fails when installed - import tempfile - # unbuffered not allowed for text in python3, so use `1 for line buffering - log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log') + log_file_path = pathlib.Path(config.app_dir_path / 'logs') + log_file_path.mkdir(exist_ok=True) + log_file_path /= f'{appname}.log' + sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here. # TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup @@ -64,6 +67,7 @@ from config import appversion, appversion_nobuild, config, copyright from EDMCLogging import edmclogger, logger, logging from journal_lock import JournalLock, JournalLockResult +from update import check_for_fdev_updates if __name__ == '__main__': # noqa: C901 # Command-line arguments @@ -251,24 +255,16 @@ if __name__ == '__main__': # noqa: C901 logger.trace_if('frontier-auth.windows', 'Begin...') if sys.platform == 'win32': - # If *this* instance hasn't locked, then another already has and we # now need to do the edmc:// checks for auth callback if locked != JournalLockResult.LOCKED: - from ctypes import windll, c_int, create_unicode_buffer, WINFUNCTYPE - from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR + from ctypes import windll, create_unicode_buffer, WINFUNCTYPE + from ctypes.wintypes import BOOL, HWND, LPARAM + import win32gui + import win32api + import win32con - EnumWindows = windll.user32.EnumWindows # noqa: N806 - GetClassName = windll.user32.GetClassNameW # noqa: N806 - GetClassName.argtypes = [HWND, LPWSTR, c_int] - GetWindowText = windll.user32.GetWindowTextW # noqa: N806 - GetWindowText.argtypes = [HWND, LPWSTR, c_int] - GetWindowTextLength = windll.user32.GetWindowTextLengthW # noqa: N806 GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 - - SW_RESTORE = 9 # noqa: N806 - SetForegroundWindow = windll.user32.SetForegroundWindow # noqa: N806 - ShowWindow = windll.user32.ShowWindow # noqa: N806 ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 COINIT_MULTITHREADED = 0 # noqa: N806,F841 @@ -276,16 +272,9 @@ if __name__ == '__main__': # noqa: C901 COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806 CoInitializeEx = windll.ole32.CoInitializeEx # noqa: N806 - ShellExecute = windll.shell32.ShellExecuteW # noqa: N806 - ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT] - def window_title(h: int) -> str | None: if h: - text_length = GetWindowTextLength(h) + 1 - buf = create_unicode_buffer(text_length) - if GetWindowText(h, buf, text_length): - return buf.value - + return win32gui.GetWindowText(h) return None @WINFUNCTYPE(BOOL, HWND, LPARAM) @@ -307,7 +296,7 @@ if __name__ == '__main__': # noqa: C901 # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 cls = create_unicode_buffer(257) # This conditional is exploded to make debugging slightly easier - if GetClassName(window_handle, cls, 257): + if win32gui.GetClassName(window_handle): if cls.value == 'TkTopLevel': if window_title(window_handle) == applongname: if GetProcessHandleFromHwnd(window_handle): @@ -315,12 +304,12 @@ if __name__ == '__main__': # noqa: C901 if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler_redirect): CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) # Wait for it to be responsive to avoid ShellExecute recursing - ShowWindow(window_handle, SW_RESTORE) - ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) + win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) + win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) else: - ShowWindowAsync(window_handle, SW_RESTORE) - SetForegroundWindow(window_handle) + ShowWindowAsync(window_handle, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(window_handle) return False # Indicate window found, so stop iterating @@ -332,26 +321,17 @@ if __name__ == '__main__': # noqa: C901 # enumwindwsproc() on each. When an invocation returns False it # stops iterating. # Ref: - EnumWindows(enumwindowsproc, 0) + win32gui.EnumWindows(enumwindowsproc, 0) def already_running_popup(): """Create the "already running" popup.""" + from tkinter import messagebox # Check for CL arg that suppresses this popup. if args.suppress_dupe_process_popup: sys.exit(0) - root = tk.Tk(className=appname.lower()) - - frame = tk.Frame(root) - frame.grid(row=1, column=0, sticky=tk.NSEW) - - label = tk.Label(frame, text='An EDMarketConnector.exe process was already running, exiting.') - label.grid(row=1, column=0, sticky=tk.NSEW) - - button = ttk.Button(frame, text='OK', command=lambda: sys.exit(0)) - button.grid(row=2, column=0, sticky=tk.S) - - root.mainloop() + messagebox.showerror(title=appname, message="An EDMarketConnector process was already running, exiting.") + sys.exit(0) journal_lock = JournalLock() locked = journal_lock.obtain_lock() @@ -403,12 +383,9 @@ if TYPE_CHECKING: from logging import TRACE # type: ignore # noqa: F401 # Needed to update mypy if sys.platform == 'win32': - from infi.systray import SysTrayIcon + from simplesystray import SysTrayIcon # isort: on - def _(x: str) -> str: - """Fake the l10n translation functions for typing.""" - return x import tkinter as tk import tkinter.filedialog @@ -425,28 +402,13 @@ from commodity import COMMODITY_CSV from dashboard import dashboard from edmc_data import ship_name_map from hotkey import hotkeymgr -from l10n import Translations +from l10n import translations as tr from monitor import monitor from theme import theme -from ttkHyperlinkLabel import HyperlinkLabel +from ttkHyperlinkLabel import HyperlinkLabel, SHIPYARD_HTML_TEMPLATE SERVER_RETRY = 5 # retry pause for Companion servers [s] -SHIPYARD_HTML_TEMPLATE = """ - - - - - Redirecting you to your {ship_name} at {provider_name}... - - - - You should be redirected to your {ship_name} at {provider_name} shortly... - - - -""" - class AppWindow: """Define the main application window.""" @@ -477,9 +439,9 @@ class AppWindow: self.prefsdialog = None if sys.platform == 'win32': - from infi.systray import SysTrayIcon + from simplesystray import SysTrayIcon - def open_window(systray: 'SysTrayIcon') -> None: + def open_window(systray: 'SysTrayIcon', *args) -> None: self.w.deiconify() menu_options = (("Open", None, open_window),) @@ -489,21 +451,20 @@ class AppWindow: plug.load_plugins(master) - if sys.platform != 'darwin': - if sys.platform == 'win32': - self.w.wm_iconbitmap(default='EDMarketConnector.ico') + if sys.platform == 'win32': + self.w.wm_iconbitmap(default='EDMarketConnector.ico') - else: - self.w.tk.call('wm', 'iconphoto', self.w, '-default', - tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png'))) + else: + image_path = config.respath_path / 'io.edcd.EDMarketConnector.png' + self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=image_path)) - # TODO: Export to files and merge from them in future ? - self.theme_icon = tk.PhotoImage( - data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 - self.theme_minimize = tk.BitmapImage( - data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 - self.theme_close = tk.BitmapImage( - data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + # TODO: Export to files and merge from them in future ? + self.theme_icon = tk.PhotoImage( + data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 + self.theme_minimize = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + self.theme_close = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) @@ -512,13 +473,13 @@ class AppWindow: self.cmdr_label = tk.Label(frame, name='cmdr_label') self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr') self.ship_label = tk.Label(frame, name='ship_label') - self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship') + self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship', popup_copy=True) self.suit_label = tk.Label(frame, name='suit_label') self.suit = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='suit') self.system_label = tk.Label(frame, name='system_label') self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True, name='system') self.station_label = tk.Label(frame, name='station_label') - self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station') + self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station', popup_copy=True) # system and station text is set/updated by the 'provider' plugins # edsm and inara. Look for: # @@ -584,7 +545,7 @@ class AppWindow: self.button = ttk.Button( frame, name='update_button', - text=_('Update'), # LANG: Main UI Update button + text=tr.tl('Update'), # LANG: Main UI Update button width=28, default=tk.ACTIVE, state=tk.DISABLED @@ -592,7 +553,7 @@ class AppWindow: self.theme_button = tk.Label( frame, name='themed_update_button', - width=32 if sys.platform == 'darwin' else 28, + width=28, state=tk.DISABLED ) @@ -626,156 +587,115 @@ class AppWindow: self.updater = update.Updater(tkroot=self.w) self.updater.check_for_updates() # Sparkle / WinSparkle does this automatically for packaged apps - if sys.platform == 'darwin': - # Can't handle (de)iconify if topmost is set, so suppress iconify button - # http://wiki.tcl.tk/13428 and p15 of - # https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf - root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable') + self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) + self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status)) + self.file_menu.add_command(command=self.save_raw) + self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs)) + self.file_menu.add_separator() + self.file_menu.add_command(command=self.onexit) + self.menubar.add_cascade(menu=self.file_menu) + self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) + self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) + self.menubar.add_cascade(menu=self.edit_menu) + self.help_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) # type: ignore + self.help_menu.add_command(command=self.help_general) # Documentation + self.help_menu.add_command(command=self.help_troubleshooting) # Troubleshooting + self.help_menu.add_command(command=self.help_report_a_bug) # Report A Bug + self.help_menu.add_command(command=self.help_privacy) # Privacy Policy + self.help_menu.add_command(command=self.help_releases) # Release Notes + self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates... + # About E:D Market Connector + self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w)) + logfile_loc = pathlib.Path(config.app_dir_path / 'logs') + self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder + self.help_menu.add_command(command=lambda: prefs.help_open_system_profiler(self)) # Open Log Folde - # https://www.tcl.tk/man/tcl/TkCmd/menu.htm - self.system_menu = tk.Menu(self.menubar, name='apple') - self.system_menu.add_command(command=lambda: self.w.call('tk::mac::standardAboutPanel')) - self.system_menu.add_command(command=lambda: self.updater.check_for_updates()) + self.menubar.add_cascade(menu=self.help_menu) + if sys.platform == 'win32': + # Must be added after at least one "real" menu entry + self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) + self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) + self.system_menu.add_separator() + # LANG: Appearance - Label for checkbox to select if application always on top + self.system_menu.add_checkbutton(label=tr.tl('Always on top'), + variable=self.always_ontop, + command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) - self.file_menu = tk.Menu(self.menubar, name='file') - self.file_menu.add_command(command=self.save_raw) - self.menubar.add_cascade(menu=self.file_menu) - self.edit_menu = tk.Menu(self.menubar, name='edit') - self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) - self.menubar.add_cascade(menu=self.edit_menu) - self.w.bind('', self.copy) - self.view_menu = tk.Menu(self.menubar, name='view') - self.view_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status)) - self.menubar.add_cascade(menu=self.view_menu) - window_menu = tk.Menu(self.menubar, name='window') - self.menubar.add_cascade(menu=window_menu) - self.help_menu = tk.Menu(self.menubar, name='help') - self.w.createcommand("::tk::mac::ShowHelp", self.help_general) - self.help_menu.add_command(command=self.help_troubleshooting) - self.help_menu.add_command(command=self.help_report_a_bug) - self.help_menu.add_command(command=self.help_privacy) - self.help_menu.add_command(command=self.help_releases) - self.menubar.add_cascade(menu=self.help_menu) - self.w['menu'] = self.menubar - # https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm - self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0') - self.w.createcommand('tkAboutDialog', lambda: self.w.call('tk::mac::standardAboutPanel')) - self.w.createcommand("::tk::mac::Quit", self.onexit) - self.w.createcommand("::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.postprefs)) - self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore - self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app - self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis - else: - self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) - self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status)) - self.file_menu.add_command(command=self.save_raw) - self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs)) - self.file_menu.add_separator() - self.file_menu.add_command(command=self.onexit) - self.menubar.add_cascade(menu=self.file_menu) - self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) - self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) - self.menubar.add_cascade(menu=self.edit_menu) - self.help_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) # type: ignore - self.help_menu.add_command(command=self.help_general) # Documentation - self.help_menu.add_command(command=self.help_troubleshooting) # Troubleshooting - self.help_menu.add_command(command=self.help_report_a_bug) # Report A Bug - self.help_menu.add_command(command=self.help_privacy) # Privacy Policy - self.help_menu.add_command(command=self.help_releases) # Release Notes - self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates... - # About E:D Market Connector - self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w)) - self.help_menu.add_command(command=prefs.help_open_log_folder) # Open Log Folder + self.w.bind('', self.copy) - self.menubar.add_cascade(menu=self.help_menu) - if sys.platform == 'win32': - # Must be added after at least one "real" menu entry - self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) - self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) - self.system_menu.add_separator() - # LANG: Appearance - Label for checkbox to select if application always on top - self.system_menu.add_checkbutton(label=_('Always on top'), - variable=self.always_ontop, - command=self.ontop_changed) # Appearance setting - self.menubar.add_cascade(menu=self.system_menu) - self.w.bind('', self.copy) + # Bind to the Default theme minimise button + self.w.bind("", self.default_iconify) - # Bind to the Default theme minimise button - self.w.bind("", self.default_iconify) + self.w.protocol("WM_DELETE_WINDOW", self.onexit) + theme.register(self.menubar) # menus and children aren't automatically registered + theme.register(self.file_menu) + theme.register(self.edit_menu) + theme.register(self.help_menu) - self.w.protocol("WM_DELETE_WINDOW", self.onexit) - theme.register(self.menubar) # menus and children aren't automatically registered - theme.register(self.file_menu) - theme.register(self.edit_menu) - theme.register(self.help_menu) - - # Alternate title bar and menu for dark theme - self.theme_menubar = tk.Frame(frame, name="alternate_menubar") - self.theme_menubar.columnconfigure(2, weight=1) - theme_titlebar = tk.Label( - self.theme_menubar, - name="alternate_titlebar", - text=applongname, - image=self.theme_icon, cursor='fleur', - anchor=tk.W, compound=tk.LEFT - ) - theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) - self.drag_offset: tuple[int | None, int | None] = (None, None) - theme_titlebar.bind('', self.drag_start) - theme_titlebar.bind('', self.drag_continue) - theme_titlebar.bind('', self.drag_end) - theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) - theme_minimize.grid(row=0, column=3, padx=2) - theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) - theme_close = tk.Label(self.theme_menubar, image=self.theme_close) - theme_close.grid(row=0, column=4, padx=2) - theme.button_bind(theme_close, self.onexit, image=self.theme_close) - self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) - self.theme_file_menu.grid(row=1, column=0, padx=self.PADX, sticky=tk.W) - theme.button_bind(self.theme_file_menu, - lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), - e.widget.winfo_rooty() - + e.widget.winfo_height())) - self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) - self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) - theme.button_bind(self.theme_edit_menu, - lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), - e.widget.winfo_rooty() - + e.widget.winfo_height())) - self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W) - self.theme_help_menu.grid(row=1, column=2, sticky=tk.W) - theme.button_bind(self.theme_help_menu, - lambda e: self.help_menu.tk_popup(e.widget.winfo_rootx(), - e.widget.winfo_rooty() - + e.widget.winfo_height())) - tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=self.PADX, sticky=tk.EW) - theme.register(self.theme_minimize) # images aren't automatically registered - theme.register(self.theme_close) - self.blank_menubar = tk.Frame(frame, name="blank_menubar") - tk.Label(self.blank_menubar).grid() - tk.Label(self.blank_menubar).grid() - tk.Frame(self.blank_menubar, height=2).grid() - theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar), - {'row': 0, 'columnspan': 2, 'sticky': tk.NSEW}) - self.w.resizable(tk.TRUE, tk.FALSE) + # Alternate title bar and menu for dark theme + self.theme_menubar = tk.Frame(frame, name="alternate_menubar") + self.theme_menubar.columnconfigure(2, weight=1) + theme_titlebar = tk.Label( + self.theme_menubar, + name="alternate_titlebar", + text=applongname, + image=self.theme_icon, cursor='fleur', + anchor=tk.W, compound=tk.LEFT + ) + theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) + self.drag_offset: tuple[int | None, int | None] = (None, None) + theme_titlebar.bind('', self.drag_start) + theme_titlebar.bind('', self.drag_continue) + theme_titlebar.bind('', self.drag_end) + theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) + theme_minimize.grid(row=0, column=3, padx=2) + theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) + theme_close = tk.Label(self.theme_menubar, image=self.theme_close) + theme_close.grid(row=0, column=4, padx=2) + theme.button_bind(theme_close, self.onexit, image=self.theme_close) + self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) + self.theme_file_menu.grid(row=1, column=0, padx=self.PADX, sticky=tk.W) + theme.button_bind(self.theme_file_menu, + lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), + e.widget.winfo_rooty() + + e.widget.winfo_height())) + self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) + self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) + theme.button_bind(self.theme_edit_menu, + lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), + e.widget.winfo_rooty() + + e.widget.winfo_height())) + self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W) + self.theme_help_menu.grid(row=1, column=2, sticky=tk.W) + theme.button_bind(self.theme_help_menu, + lambda e: self.help_menu.tk_popup(e.widget.winfo_rootx(), + e.widget.winfo_rooty() + + e.widget.winfo_height())) + tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=self.PADX, sticky=tk.EW) + theme.register(self.theme_minimize) # images aren't automatically registered + theme.register(self.theme_close) + self.blank_menubar = tk.Frame(frame, name="blank_menubar") + tk.Label(self.blank_menubar).grid() + tk.Label(self.blank_menubar).grid() + tk.Frame(self.blank_menubar, height=2).grid() + theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar), + {'row': 0, 'columnspan': 2, 'sticky': tk.NSEW}) + self.w.resizable(tk.TRUE, tk.FALSE) # update geometry if config.get_str('geometry'): match = re.match(r'\+([\-\d]+)\+([\-\d]+)', config.get_str('geometry')) if match: - if sys.platform == 'darwin': - # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 - if int(match.group(2)) >= 0: - self.w.geometry(config.get_str('geometry')) - elif sys.platform == 'win32': + if sys.platform == 'win32': # Check that the titlebar will be at least partly on screen - import ctypes - from ctypes.wintypes import POINT + import win32api + import win32con + x = int(match.group(1)) + 16 + y = int(match.group(2)) + 16 + point = (x, y) # https://msdn.microsoft.com/en-us/library/dd145064 - MONITOR_DEFAULTTONULL = 0 # noqa: N806 - if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), - MONITOR_DEFAULTTONULL): + if win32api.MonitorFromPoint(point, win32con.MONITOR_DEFAULTTONULL): self.w.geometry(config.get_str('geometry')) else: self.w.geometry(config.get_str('geometry')) @@ -800,6 +720,11 @@ class AppWindow: self.w.bind_all('<>', self.auth) # cAPI auth self.w.bind_all('<>', self.onexit) # Updater + # Check for Valid Providers + validate_providers() + if monitor.cmdr is None: + self.status['text'] = tr.tl("Awaiting Full CMDR Login") # LANG: Await Full CMDR Login to Game + # Start a protocol handler to handle cAPI registration. Requires main loop to be running. self.w.after_idle(lambda: protocol.protocolhandler.start(self.w)) @@ -828,7 +753,7 @@ class AppWindow: suit = monitor.state.get('SuitCurrent') if suit is None: - self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit + self.suit['text'] = f'<{tr.tl("Unknown")}>' # LANG: Unknown suit return suitname = suit['edmcName'] @@ -865,7 +790,7 @@ class AppWindow: self.suit.grid_forget() self.suit_shown = False - def postprefs(self, dologin: bool = True): + def postprefs(self, dologin: bool = True, **postargs): """Perform necessary actions after the Preferences dialog is applied.""" self.prefsdialog = None self.set_labels() # in case language has changed @@ -884,66 +809,80 @@ class AppWindow: # (Re-)install log monitoring if not monitor.start(self.w): # LANG: ED Journal file location appears to be in error - self.status['text'] = _('Error: Check E:D journal file location') + self.status['text'] = tr.tl('Error: Check E:D journal file location') if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr + if postargs.get('Update') and postargs.get('Track'): + track = postargs.get('Track') + # LANG: Inform the user the Update Track has changed + self.status['text'] = tr.tl('Update Track Changed to {TRACK}').format(TRACK=track) + self.updater.check_for_updates() + if track == "Stable": + # LANG: Inform the user the Update Track has changed + title = tr.tl('Update Track Changed to {TRACK}').format(TRACK=track) + update_msg = tr.tl( # LANG: Inform User of Beta -> Stable Transition Risks + 'Update track changed to Stable from Beta. ' + 'You will no longer receive Beta updates. You will stay on your current Beta ' + r'version until the next Stable release.\r\n\r\n' + 'You can manually revert to the latest Stable version. To do so, you must download and install ' + 'the latest Stable version manually. Note that this may introduce bugs or break completely' + r' if downgrading between major versions with significant changes.\r\n\r\n' + 'Do you want to open GitHub to download the latest release?' + ) + update_msg = update_msg.replace('\\n', '\n') + update_msg = update_msg.replace('\\r', '\r') + stable_popup = tk.messagebox.askyesno(title=title, message=update_msg) + if stable_popup: + webbrowser.open("https://github.com/EDCD/eDMarketConnector/releases/latest") + if postargs.get('Restart_Req'): + # LANG: Text of Notification Popup for EDMC Restart + restart_msg = tr.tl('A restart of EDMC is required. EDMC will now restart.') + restart_box = tk.messagebox.Message( + title=tr.tl('Restart Required'), # LANG: Title of Notification Popup for EDMC Restart + message=restart_msg, + type=tk.messagebox.OK + ) + restart_box.show() + if restart_box: + app.onexit(restart=True) + def set_labels(self): """Set main window labels, e.g. after language change.""" - self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Label for commander name in main window + self.cmdr_label['text'] = tr.tl('Cmdr') + ':' # LANG: Label for commander name in main window # LANG: 'Ship' or multi-crew role label in main window, as applicable - self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window - self.suit_label['text'] = _('Suit') + ':' # LANG: Label for 'Suit' line in main UI - self.system_label['text'] = _('System') + ':' # LANG: Label for 'System' line in main UI - self.station_label['text'] = _('Station') + ':' # LANG: Label for 'Station' line in main UI - self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window - if sys.platform == 'darwin': - 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=_('Documentation')) # LANG: Help > Documentation - self.help_menu.entryconfigure(2, label=_('Troubleshooting')) # LANG: Help > Troubleshooting - self.help_menu.entryconfigure(3, label=_('Report A Bug')) # LANG: Help > Report A Bug - self.help_menu.entryconfigure(4, label=_('Privacy Policy')) # LANG: Help > Privacy Policy - self.help_menu.entryconfigure(5, label=_('Release Notes')) # LANG: Help > Release Notes - self.help_menu.entryconfigure(6, label=_('Open Log Folder')) # LANG: Help > Open Log Folder + self.ship_label['text'] = (monitor.state['Captain'] and tr.tl('Role') or tr.tl('Ship')) + ':' # Main window + self.suit_label['text'] = tr.tl('Suit') + ':' # LANG: Label for 'Suit' line in main UI + self.system_label['text'] = tr.tl('System') + ':' # LANG: Label for 'System' line in main UI + self.station_label['text'] = tr.tl('Station') + ':' # LANG: Label for 'Station' line in main UI + self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window + self.menubar.entryconfigure(1, label=tr.tl('File')) # LANG: 'File' menu title + self.menubar.entryconfigure(2, label=tr.tl('Edit')) # LANG: 'Edit' menu title + self.menubar.entryconfigure(3, label=tr.tl('Help')) # LANG: 'Help' menu title + self.theme_file_menu['text'] = tr.tl('File') # LANG: 'File' menu title + self.theme_edit_menu['text'] = tr.tl('Edit') # LANG: 'Edit' menu title + self.theme_help_menu['text'] = tr.tl('Help') # LANG: 'Help' menu title - else: - 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=tr.tl('Status')) # LANG: File > Status + self.file_menu.entryconfigure(1, label=tr.tl('Save Raw Data...')) # LANG: File > Save Raw Data... + self.file_menu.entryconfigure(2, label=tr.tl('Settings')) # LANG: File > Settings + self.file_menu.entryconfigure(4, label=tr.tl('Exit')) # LANG: File > Exit - # File menu - self.file_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status - self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: File > Save Raw Data... - self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: File > Settings - self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: File > Exit - - # Help menu - self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help > Documentation - self.help_menu.entryconfigure(1, label=_('Troubleshooting')) # LANG: Help > Troubleshooting - self.help_menu.entryconfigure(2, label=_('Report A Bug')) # LANG: Help > Report A Bug - self.help_menu.entryconfigure(3, label=_('Privacy Policy')) # LANG: Help > Privacy Policy - self.help_menu.entryconfigure(4, label=_('Release Notes')) # LANG: Help > Release Notes - self.help_menu.entryconfigure(5, label=_('Check for Updates...')) # LANG: Help > Check for Updates... - self.help_menu.entryconfigure(6, label=_("About {APP}").format(APP=applongname)) # LANG: Help > About App - self.help_menu.entryconfigure(7, label=_('Open Log Folder')) # LANG: Help > Open Log Folder + # Help menu + self.help_menu.entryconfigure(0, label=tr.tl('Documentation')) # LANG: Help > Documentation + self.help_menu.entryconfigure(1, label=tr.tl('Troubleshooting')) # LANG: Help > Troubleshooting + self.help_menu.entryconfigure(2, label=tr.tl('Report A Bug')) # LANG: Help > Report A Bug + self.help_menu.entryconfigure(3, label=tr.tl('Privacy Policy')) # LANG: Help > Privacy Policy + self.help_menu.entryconfigure(4, label=tr.tl('Release Notes')) # LANG: Help > Release Notes + self.help_menu.entryconfigure(5, label=tr.tl('Check for Updates...')) # LANG: Help > Check for Updates... + self.help_menu.entryconfigure(6, label=tr.tl("About {APP}").format(APP=applongname)) # LANG: Help > About App + self.help_menu.entryconfigure(7, label=tr.tl('Open Log Folder')) # LANG: Help > Open Log Folder + self.help_menu.entryconfigure(8, label=tr.tl('Open System Profiler')) # LANG: Help > Open System Profiler # Edit menu - self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: Label for 'Copy' as in 'Copy and Paste' + self.edit_menu.entryconfigure(0, label=tr.tl('Copy')) # LANG: Label for 'Copy' as in 'Copy and Paste' def login(self): """Initiate CAPI/Frontier login and set other necessary state.""" @@ -954,36 +893,26 @@ class AppWindow: if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') # LANG: CAPI auth aborted because of killswitch - self.status['text'] = _('CAPI auth disabled by killswitch') + self.status['text'] = tr.tl('CAPI auth disabled by killswitch') return if not self.status['text']: # LANG: Status - Attempting to get a Frontier Auth Access Token - self.status['text'] = _('Logging in...') + self.status['text'] = tr.tl('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED - if sys.platform == 'darwin': - self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status - self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data - - else: - self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status - self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data + self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status + self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data self.w.update_idletasks() try: if companion.session.login(monitor.cmdr, monitor.is_beta): # LANG: Successfully authenticated with the Frontier website - self.status['text'] = _('Authentication successful') + self.status['text'] = tr.tl('Authentication successful') - if sys.platform == 'darwin': - self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status - self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data - - else: - self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status - self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data + self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status + self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e: self.status['text'] = str(e) @@ -1011,17 +940,17 @@ class AppWindow: # Signal as error because the user might actually be docked # but the server hosting the Companion API hasn't caught up # LANG: Player is not docked at a station, when we expect them to be - self._handle_status(_("You're not docked at a station!")) + self._handle_status(tr.tl("You're not docked at a station!")) return False # Ignore possibly missing shipyard info if output_flags & config.OUT_EDDN_SEND_STATION_DATA and not (has_commodities or has_modules): # LANG: Status - Either no market or no modules data for station from Frontier CAPI - self._handle_status(_("Station doesn't have anything!")) + self._handle_status(tr.tl("Station doesn't have anything!")) elif not has_commodities: # LANG: Status - No station market data from Frontier CAPI - self._handle_status(_("Station doesn't have a market!")) + self._handle_status(tr.tl("Station doesn't have a market!")) elif output_flags & commodities_flag: # Fixup anomalies in the comodity data @@ -1059,7 +988,7 @@ class AppWindow: if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') # LANG: CAPI auth query aborted because of killswitch - self.status['text'] = _('CAPI auth disabled by killswitch') + self.status['text'] = tr.tl('CAPI auth disabled by killswitch') hotkeymgr.play_bad() return @@ -1069,37 +998,37 @@ class AppWindow: if not monitor.cmdr: logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown') # LANG: CAPI queries aborted because Cmdr name is unknown - self.status['text'] = _('CAPI query aborted: Cmdr name unknown') + self.status['text'] = tr.tl('CAPI query aborted: Cmdr name unknown') return if not monitor.mode: logger.trace_if('capi.worker', 'Aborting Query: Game Mode unknown') # LANG: CAPI queries aborted because game mode unknown - self.status['text'] = _('CAPI query aborted: Game mode unknown') + self.status['text'] = tr.tl('CAPI query aborted: Game mode unknown') return if monitor.state['GameVersion'] is None: logger.trace_if('capi.worker', 'Aborting Query: GameVersion unknown') # LANG: CAPI queries aborted because GameVersion unknown - self.status['text'] = _('CAPI query aborted: GameVersion unknown') + self.status['text'] = tr.tl('CAPI query aborted: GameVersion unknown') return if not monitor.state['SystemName']: logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown') # LANG: CAPI queries aborted because current star system name unknown - self.status['text'] = _('CAPI query aborted: Current system unknown') + self.status['text'] = tr.tl('CAPI query aborted: Current system unknown') return if monitor.state['Captain']: logger.trace_if('capi.worker', 'Aborting Query: In multi-crew') # LANG: CAPI queries aborted because player is in multi-crew on other Cmdr's ship - self.status['text'] = _('CAPI query aborted: In other-ship multi-crew') + self.status['text'] = tr.tl('CAPI query aborted: In other-ship multi-crew') return if monitor.mode == 'CQC': logger.trace_if('capi.worker', 'Aborting Query: In CQC') # LANG: CAPI queries aborted because player is in CQC (Arena) - self.status['text'] = _('CAPI query aborted: CQC (Arena) detected') + self.status['text'] = tr.tl('CAPI query aborted: CQC (Arena) detected') return if companion.session.state == companion.Session.STATE_AUTH: @@ -1120,7 +1049,7 @@ class AppWindow: hotkeymgr.play_good() # LANG: Status - Attempting to retrieve data from Frontier CAPI - self.status['text'] = _('Fetching data...') + self.status['text'] = tr.tl('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() @@ -1138,7 +1067,7 @@ class AppWindow: """ Perform CAPI fleetcarrier data retrieval and associated actions. - This is triggered by certain FleetCarrier journal events + This is triggered by certain Fleet Carrier journal events :param event: Tk generated event details. """ @@ -1150,20 +1079,20 @@ class AppWindow: if should_return: logger.warning('capi.fleetcarrier has been disabled via killswitch. Returning.') # LANG: CAPI fleetcarrier query aborted because of killswitch - self.status['text'] = _('CAPI fleetcarrier disabled by killswitch') + self.status['text'] = tr.tl('CAPI fleetcarrier disabled by killswitch') hotkeymgr.play_bad() return if not monitor.cmdr: logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown') # LANG: CAPI fleetcarrier query aborted because Cmdr name is unknown - self.status['text'] = _('CAPI query aborted: Cmdr name unknown') + self.status['text'] = tr.tl('CAPI query aborted: Cmdr name unknown') return if monitor.state['GameVersion'] is None: logger.trace_if('capi.worker', 'Aborting Query: GameVersion unknown') # LANG: CAPI fleetcarrier query aborted because GameVersion unknown - self.status['text'] = _('CAPI query aborted: GameVersion unknown') + self.status['text'] = tr.tl('CAPI query aborted: GameVersion unknown') return if not companion.session.retrying: @@ -1172,11 +1101,11 @@ class AppWindow: return # LANG: Status - Attempting to retrieve data from Frontier CAPI - self.status['text'] = _('Fetching data...') + self.status['text'] = tr.tl('Fetching data...') self.w.update_idletasks() query_time = int(time()) - logger.trace_if('capi.worker', 'Requesting fleetcarrier data') + logger.trace_if('capi.worker', 'Requesting Fleet Carrier data') config.set('fleetcarrierquerytime', query_time) logger.trace_if('capi.worker', 'Calling companion.session.fleetcarrier') companion.session.fleetcarrier( @@ -1215,11 +1144,11 @@ class AppWindow: # Validation if 'name' not in capi_response.capi_data: # LANG: No data was returned for the fleetcarrier from the Frontier CAPI - err = self.status['text'] = _('CAPI: No fleetcarrier data returned') + err = self.status['text'] = tr.tl('CAPI: No Fleet Carrier data returned') elif not capi_response.capi_data.get('name', {}).get('callsign'): # LANG: We didn't have the fleetcarrier callsign when we should have - err = self.status['text'] = _("CAPI: Fleetcarrier data incomplete") # Shouldn't happen + err = self.status['text'] = tr.tl("CAPI: Fleet Carrier data incomplete") # Shouldn't happen else: if __debug__: # Recording @@ -1238,24 +1167,24 @@ class AppWindow: elif 'commander' not in capi_response.capi_data: # This can happen with EGS Auth if no commander created yet # LANG: No data was returned for the commander from the Frontier CAPI - err = self.status['text'] = _('CAPI: No commander data returned') + err = self.status['text'] = tr.tl('CAPI: No commander data returned') elif not capi_response.capi_data.get('commander', {}).get('name'): # LANG: We didn't have the commander name when we should have - err = self.status['text'] = _("Who are you?!") # Shouldn't happen + err = self.status['text'] = tr.tl("Who are you?!") # Shouldn't happen elif (not capi_response.capi_data.get('lastSystem', {}).get('name') or (capi_response.capi_data['commander'].get('docked') and not capi_response.capi_data.get('lastStarport', {}).get('name'))): # LANG: We don't know where the commander is, when we should - err = self.status['text'] = _("Where are you?!") # Shouldn't happen + err = self.status['text'] = tr.tl("Where are you?!") # Shouldn't happen elif ( not capi_response.capi_data.get('ship', {}).get('name') or not capi_response.capi_data.get('ship', {}).get('modules') ): # LANG: We don't know what ship the commander is in, when we should - err = self.status['text'] = _("What are you flying?!") # Shouldn't happen + err = self.status['text'] = tr.tl("What are you flying?!") # Shouldn't happen elif monitor.cmdr and capi_response.capi_data['commander']['name'] != monitor.cmdr: # Companion API Commander doesn't match Journal @@ -1382,7 +1311,7 @@ class AppWindow: except companion.ServerConnectionError as comp_err: # LANG: Frontier CAPI server error when fetching data - self.status['text'] = _('Frontier CAPI server error') + self.status['text'] = tr.tl('Frontier CAPI server error') logger.warning(f'Exception while contacting server: {comp_err}') err = self.status['text'] = str(comp_err) play_bad = True @@ -1391,7 +1320,7 @@ class AppWindow: # We need to 'close' the auth else it'll see STATE_OK and think login() isn't needed companion.session.reinit_session() # LANG: Frontier CAPI Access Token expired, trying to get a new one - self.status['text'] = _('CAPI: Refreshing access token...') + self.status['text'] = tr.tl('CAPI: Refreshing access token...') if companion.session.login(): logger.debug('Initial query failed, but login() just worked, trying again...') companion.session.retrying = True @@ -1430,7 +1359,7 @@ class AppWindow: if not err: # not self.status['text']: # no errors # LANG: Time when we last obtained Frontier CAPI data - self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(capi_response.query_time)) + self.status['text'] = strftime(tr.tl('Last updated at %H:%M:%S'), localtime(capi_response.query_time)) if capi_response.play_sound and play_bad: hotkeymgr.play_bad() @@ -1458,9 +1387,9 @@ class AppWindow: return { None: '', 'Idle': '', - 'FighterCon': _('Fighter'), # LANG: Multicrew role - 'FireCon': _('Gunner'), # LANG: Multicrew role - 'FlightCon': _('Helm'), # LANG: Multicrew role + 'FighterCon': tr.tl('Fighter'), # LANG: Multicrew role + 'FireCon': tr.tl('Gunner'), # LANG: Multicrew role + 'FlightCon': tr.tl('Helm'), # LANG: Multicrew role }.get(role, role) if monitor.thread is None: @@ -1483,7 +1412,7 @@ class AppWindow: else: self.cmdr['text'] = f'{monitor.cmdr}' - self.ship_label['text'] = _('Role') + ':' # LANG: Multicrew role label in main window + self.ship_label['text'] = tr.tl('Role') + ':' # LANG: Multicrew role label in main window self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None) elif monitor.cmdr: @@ -1493,7 +1422,7 @@ class AppWindow: else: self.cmdr['text'] = monitor.cmdr - self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI + self.ship_label['text'] = tr.tl('Ship') + ':' # LANG: 'Ship' label in main UI # TODO: Show something else when on_foot if monitor.state['ShipName']: @@ -1516,7 +1445,7 @@ class AppWindow: else: self.cmdr['text'] = '' - self.ship_label['text'] = _('Ship') + ':' # LANG: 'Ship' label in main UI + self.ship_label['text'] = tr.tl('Ship') + ':' # LANG: 'Ship' label in main UI self.ship['text'] = '' if monitor.cmdr and monitor.is_beta: @@ -1547,7 +1476,7 @@ class AppWindow: self.w.update_idletasks() # Companion login - if entry['event'] in [None, 'StartUp', 'NewCommander', 'LoadGame'] and monitor.cmdr: + if entry['event'] in (None, 'StartUp', 'NewCommander', 'LoadGame') and monitor.cmdr: if not config.get_list('cmdrs') or monitor.cmdr not in config.get_list('cmdrs'): config.set('cmdrs', config.get_list('cmdrs', default=[]) + [monitor.cmdr]) self.login() @@ -1565,7 +1494,7 @@ class AppWindow: logger.trace_if('journal.queue', 'Startup, returning') return # Startup - if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: + if entry['event'] in ('StartUp', 'LoadGame') and monitor.started: logger.info('StartUp or LoadGame event') # Disable WinSparkle automatic update checks, IFF configured to do so when in-game @@ -1653,14 +1582,9 @@ class AppWindow: try: companion.session.auth_callback() # LANG: Successfully authenticated with the Frontier website - self.status['text'] = _('Authentication successful') - if sys.platform == 'darwin': - self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status - self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data - - else: - self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status - self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data + self.status['text'] = tr.tl('Authentication successful') + self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status + self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data except companion.ServerError as e: self.status['text'] = str(e) @@ -1699,7 +1623,7 @@ class AppWindow: hotkeymgr.play_bad() def shipyard_url(self, shipname: str) -> str | None: - """Despatch a ship URL to the configured handler.""" + """Dispatch a ship URL to the configured handler.""" if not (loadout := monitor.ship()): logger.warning('No ship loadout, aborting.') return '' @@ -1714,7 +1638,7 @@ class AppWindow: # Avoid file length limits if possible provider = config.get_str('shipyard_provider', default='EDSY') target = plug.invoke(provider, 'EDSY', 'shipyard_url', loadout, monitor.is_beta) - file_name = path.join(config.app_dir_path, "last_shipyard.html") + file_name = config.app_dir_path / "last_shipyard.html" with open(file_name, 'w') as f: f.write(SHIPYARD_HTML_TEMPLATE.format( @@ -1726,13 +1650,13 @@ class AppWindow: return f'file://localhost/{file_name}' def system_url(self, system: str) -> str | None: - """Despatch a system URL to the configured handler.""" + """Dispatch a system URL to the configured handler.""" return plug.invoke( config.get_str('system_provider', default='EDSM'), 'EDSM', 'system_url', monitor.state['SystemName'] ) def station_url(self, station: str) -> str | None: - """Despatch a station URL to the configured handler.""" + """Dispatch a station URL to the configured handler.""" return plug.invoke( config.get_str('station_provider', default='EDSM'), 'EDSM', 'station_url', monitor.state['SystemName'], monitor.state['StationName'] @@ -1744,10 +1668,10 @@ class AppWindow: # Update button in main window cooldown_time = int(self.capi_query_holdoff_time - time()) # LANG: Cooldown on 'Update' button - self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS=cooldown_time) + self.button['text'] = self.theme_button['text'] = tr.tl('cooldown {SS}s').format(SS=cooldown_time) self.w.after(1000, self.cooldown) else: - self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window + self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window self.button['state'] = self.theme_button['state'] = ( monitor.cmdr and monitor.mode and @@ -1812,15 +1736,14 @@ class AppWindow: self.parent = parent # LANG: Help > About App - self.title(_('About {APP}').format(APP=applongname)) + self.title(tr.tl('About {APP}').format(APP=applongname)) if parent.winfo_viewable(): self.transient(parent) # position over parent # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 - if sys.platform != 'darwin' or parent.winfo_rooty() > 0: - self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}') + self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}') # remove decoration if sys.platform == 'win32': @@ -1850,7 +1773,7 @@ class AppWindow: self.appversion_label.config(state=tk.DISABLED, bg=frame.cget("background"), font="TkDefaultFont") self.appversion_label.grid(row=row, column=0, sticky=tk.E) # LANG: Help > Release Notes - self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'), + self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=tr.tl('Release Notes'), url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/' f'{appversion_nobuild()}', underline=True) @@ -1876,7 +1799,7 @@ class AppWindow: ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 # LANG: Generic 'OK' button label - button = ttk.Button(frame, text=_('OK'), command=self.apply) + button = ttk.Button(frame, text=tr.tl('OK'), command=self.apply) button.grid(row=row, column=2, sticky=tk.E) button.bind("", lambda event: self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) @@ -1904,9 +1827,6 @@ class AppWindow: """ default_extension: str = '' - if sys.platform == 'darwin': - default_extension = '.json' - timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime()) f = tkinter.filedialog.asksaveasfilename( parent=self.w, @@ -1930,7 +1850,7 @@ class AppWindow: ) exit_thread.start() - def onexit(self, event=None) -> None: + def onexit(self, event=None, restart: bool = False) -> None: """Application shutdown procedure.""" if sys.platform == 'win32': shutdown_thread = threading.Thread( @@ -1942,13 +1862,12 @@ class AppWindow: config.set_shutdown() # Signal we're in shutdown now. # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 - if sys.platform != 'darwin' or self.w.winfo_rooty() > 0: - x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267' - config.set('geometry', f'+{x}+{y}') + x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267' + 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.status['text'] = tr.tl('Shutting down...') self.w.update_idletasks() logger.info('Starting shutdown procedures...') @@ -1994,6 +1913,8 @@ class AppWindow: self.w.destroy() logger.info('Done.') + if restart: + os.execv(sys.executable, ['python'] + sys.argv) def drag_start(self, event) -> None: """Initiate dragging the window.""" @@ -2108,6 +2029,52 @@ def show_killswitch_poppup(root=None): ok_button.grid(columnspan=2, sticky=tk.EW) +def validate_providers(): + """Check if Config has an invalid provider set, and reset to default if we do.""" + reset_providers = {} + station_provider: str = config.get_str("station_provider") + if station_provider not in plug.provides('station_url'): + logger.error("Station Provider Not Valid. Setting to Default.") + config.set('station_provider', 'EDSM') + reset_providers["Station"] = (station_provider, "EDSM") + + shipyard_provider: str = config.get_str("shipyard_provider") + if shipyard_provider not in plug.provides('shipyard_url'): + logger.error("Shipyard Provider Not Valid. Setting to Default.") + config.set('shipyard_provider', 'EDSY') + reset_providers["Shipyard"] = (shipyard_provider, "EDSY") + + system_provider: str = config.get_str("system_provider") + if system_provider not in plug.provides('system_url'): + logger.error("System Provider Not Valid. Setting to Default.") + config.set('system_provider', 'EDSM') + reset_providers["System"] = (system_provider, "EDSM") + + if not reset_providers: + return + + # LANG: Popup-text about Reset Providers + popup_text = tr.tl(r'One or more of your URL Providers were invalid, and have been reset:\r\n\r\n') + for provider in reset_providers: + # LANG: Text About What Provider Was Reset + popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n') + popup_text = popup_text.format( + PROVIDER=provider, + OLDPROV=reset_providers[provider][0], + NEWPROV=reset_providers[provider][1] + ) + # And now we do need these to be actual \r\n + popup_text = popup_text.replace('\\n', '\n') + popup_text = popup_text.replace('\\r', '\r') + + tk.messagebox.showinfo( + # LANG: Popup window title for Reset Providers + tr.tl('EDMC: Default Providers Reset'), + popup_text, + parent=root + ) + + # Run the app if __name__ == "__main__": # noqa: C901 logger.info(f'Startup v{appversion()} : Running on Python v{sys.version}') @@ -2229,7 +2196,7 @@ sys.path: {sys.path}''' # Plain, not via `logger` print(f'{applongname} {appversion()}') - Translations.install(config.get_str('language')) # Can generate errors so wait til log set up + tr.install(config.get_str('language')) # Can generate errors so wait til log set up setup_killswitches(args.killswitches_file) @@ -2258,13 +2225,36 @@ sys.path: {sys.path}''' if theme.default_ui_scale is not None: root.tk.call('tk', 'scaling', theme.default_ui_scale * float(ui_scale) / 100.0) - app = AppWindow(root) + try: + app = AppWindow(root) + except Exception as err: + logger.exception(f"EDMC Critical Error: {err}") + title = tr.tl("Error") # LANG: Generic error prefix + message = tr.tl( # LANG: EDMC Critical Error Notification + "EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" + ) + err = f"{err.__class__.__name__}: {err}" # type: ignore # hijacking the existing exception detection + detail = tr.tl( # LANG: EDMC Critical Error Details + r"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" + ).format(ERR=err) + detail = detail.replace('\\n', '\n') + detail = detail.replace('\\r', '\r') + msg = tk.messagebox.askyesno( + title=title, message=message, detail=detail, icon=tkinter.messagebox.ERROR, type=tkinter.messagebox.YESNO, + parent=root + ) + if msg: + webbrowser.open( + "https://github.com/EDCD/EDMarketConnector/issues/new?" + "assignees=&labels=bug%2C+unconfirmed&projects=&template=bug_report.md&title=" + ) + os.kill(os.getpid(), signal.SIGTERM) def messagebox_broken_plugins(): """Display message about 'broken' plugins that failed to load.""" if plug.PLUGINS_broken: # LANG: Popup-text about 'broken' plugins that failed to load - popup_text = _( + popup_text = tr.tl( "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' " "tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py " r"file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by " @@ -2273,9 +2263,9 @@ sys.path: {sys.path}''' # Substitute in the other words. popup_text = popup_text.format( - PLUGINS=_('Plugins'), # LANG: Settings > Plugins tab - FILE=_('File'), # LANG: 'File' menu - SETTINGS=_('Settings'), # LANG: File > Settings + PLUGINS=tr.tl('Plugins'), # LANG: Settings > Plugins tab + FILE=tr.tl('File'), # LANG: 'File' menu + SETTINGS=tr.tl('Settings'), # LANG: File > Settings DISABLED='.disabled' ) # And now we do need these to be actual \r\n @@ -2284,8 +2274,9 @@ sys.path: {sys.path}''' tk.messagebox.showinfo( # LANG: Popup window title for list of 'broken' plugins that failed to load - _('EDMC: Broken Plugins'), - popup_text + tr.tl('EDMC: Broken Plugins'), + popup_text, + parent=root ) def messagebox_not_py3(): @@ -2293,7 +2284,7 @@ sys.path: {sys.path}''' plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0) if (plugins_not_py3_last + 86400) < int(time()) and plug.PLUGINS_not_py3: # LANG: Popup-text about 'active' plugins without Python 3.x support - popup_text = _( + popup_text = tr.tl( "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the " "list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an " "updated version available, else alert the developer that they need to update the code for " @@ -2303,9 +2294,9 @@ sys.path: {sys.path}''' # Substitute in the other words. popup_text = popup_text.format( - PLUGINS=_('Plugins'), # LANG: Settings > Plugins tab - FILE=_('File'), # LANG: 'File' menu - SETTINGS=_('Settings'), # LANG: File > Settings + PLUGINS=tr.tl('Plugins'), # LANG: Settings > Plugins tab + FILE=tr.tl('File'), # LANG: 'File' menu + SETTINGS=tr.tl('Settings'), # LANG: File > Settings DISABLED='.disabled' ) # And now we do need these to be actual \r\n @@ -2314,40 +2305,12 @@ sys.path: {sys.path}''' tk.messagebox.showinfo( # LANG: Popup window title for list of 'enabled' plugins that don't work with Python 3.x - _('EDMC: Plugins Without Python 3.x Support'), - popup_text + tr.tl('EDMC: Plugins Without Python 3.x Support'), + popup_text, + parent=root ) config.set('plugins_not_py3_last', int(time())) - def check_fdev_ids(): - """Display message about missing FDEVID files.""" - fdev_files = {'commodity.csv', 'rare_commodity.csv'} - for file in fdev_files: - fdevid_file = pathlib.Path(config.respath_path / 'FDevIDs' / file) - if fdevid_file.is_file(): - continue - # LANG: Popup-text about missing FDEVID Files - popup_text = _( - "FDevID Files not found! Some functionality regarding commodities " - r"may be disabled.\r\n\r\n Do you want to open the Wiki page on " - "how to set up submodules?" - ) - # And now we do need these to be actual \r\n - popup_text = popup_text.replace('\\n', '\n') - popup_text = popup_text.replace('\\r', '\r') - - openwikipage = tk.messagebox.askquestion( - # LANG: Popup window title for missing FDEVID files - _('FDevIDs: Missing Commodity Files'), - popup_text - ) - if openwikipage == "yes": - webbrowser.open( - "https://github.com/EDCD/EDMarketConnector/wiki/Running-from-source" - "#obtain-a-copy-of-the-application-source" - ) - break - # UI Transparency ui_transparency = config.get_int('ui_transparency') if ui_transparency == 0: @@ -2360,9 +2323,11 @@ sys.path: {sys.path}''' root.after(1, messagebox_not_py3) # Show warning popup for killswitches matching current version root.after(2, show_killswitch_poppup, root) - # Check for FDEV IDs - root.after(3, check_fdev_ids) # Start the main event loop - root.mainloop() - + try: + check_for_fdev_updates() + root.mainloop() + except KeyboardInterrupt: + logger.info("Ctrl+C Detected, Attempting Clean Shutdown") + app.onexit() logger.info('Exiting') diff --git a/FDevIDs b/FDevIDs deleted file mode 160000 index 069b09b1..00000000 --- a/FDevIDs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 069b09b1afbd2001248ad93b1481d6f6a309c6f5 diff --git a/L10n/cs.strings b/L10n/cs.strings index ba11dc39..f4d1bcea 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -61,12 +61,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Upravit"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Zobrazit"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Okno"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "NÃĄpověda"; @@ -178,13 +172,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "VÃŊchozí"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -256,9 +250,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Chyba: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Nastavení"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Vyberte, kterÃĄ data chcete uklÃĄdat"; @@ -277,9 +268,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Cesta k souboru"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Změnit..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "ProchÃĄzet..."; @@ -289,21 +277,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D umístění souboru deníku"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "KlÃĄvesovÃĄ zkratka"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "KlÃĄvesovÃĄ zkratka"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Restartujte {APP} pro aktivaci klÃĄvesovÃŊch zkratek"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} vyÅžaduje oprÃĄvnění pro pouÅžití klÃĄvesovÃŊch zkratek"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Otevřít Předvolby systÊmu"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Pouze pokud je Elite: Dangerous aktivní"; @@ -370,9 +346,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "SloÅžka pluginů"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Otevřít"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: Plugin můŞete vypnout přidÃĄním{CR}'{EXT}' do nÃĄzvu jeho sloÅžky"; diff --git a/L10n/de.strings b/L10n/de.strings index 2b7c72c4..313f893e 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "Fehler: konnte Token nicht erhalten"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC ist in einen unwiederbringlichen kritischen Fehler gelaufen. EDMC fährt zum Selbstschutz herunter!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Das hat EDMC erkannt:\n\n{ERR}\n\nMÃļchtest Du einen Bug Report auf GitHub einreichen?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPI offline wegen Wartung"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Bearbeiten"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Ansicht"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Fenster"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Hilfe"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Auf Aktualisierungen ÃŧberprÃŧfen..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Öffne System Profiler"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Speichere Originaldaten..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Kopieren"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Ausschneiden"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "EinfÃŧgen"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Alles auswählen"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "CAPI auth durch Killswitch deaktiviert"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI Fleet Carrier durch Killswitch deaktiviert"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Keine Fleet Carrier-Daten erhalten"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: Keine Carrier-Daten erhalten"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Fleet Carrier-Daten unvollständig"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Carrier-Daten unvollständig"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Keine Kommandanten-Daten erhalten"; @@ -213,12 +225,6 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Eins oder mehr deiner aktivierten Plugins hat noch keinen Support fÃŧr Python 3.x. Du kannst dir die Liste im '{PLUGINS}'-Tab unter '{FILE}' > '{SETTINGS}' ansehen. Du solltest prÃŧfen, ob es fÃŧr diese Updates gibt und ansonsten dem Entwickler Bescheid geben, dass sie ihren Code fÃŧr Python 3.x aktualisieren mÃŧssen.\n\nDu kannst ein Plugin deaktivieren, indem du dessen Ordner ein '{DISABLED}' am Ende des Namens anhängst."; -/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ -"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevID Dateien nicht gefunden! Einige Funktionen in Bezug auf Waren sind mÃļglicherweise deaktiviert.\n\nMÃļchten du die Wiki-Seite zum Einrichten von Submodulen Ãļffnen?"; - -/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ -"FDevIDs: Missing Commodity Files" = "FDevIDs: Warendateien fehlen"; - /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Plugins"; @@ -231,6 +237,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Eins oder mehr deiner aktivierten Plugins konnte nicht geladen werden. Du kannst dir die Liste im '{PLUGINS}'-Tab unter '{FILE}' > '{SETTINGS}' ansehen. Dies kÃļnnte an einer falschen Ordnerstruktur liegen. Die Datei load.py sollte sich unter plugins/PLUGIN_NAME/load.py befinden.\n\nDu kannst ein Plugin deaktivieren, indem du dessen Ordner ein '{DISABLED}' am Ende des Namens anhängst."; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Einer oder mehrere deiner URL-Anbieter waren ungÃŧltig und wurden zurÃŧckgesetzt:\n\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} war zuvor auf {OLDPROV} festgelegt und wurde zu {NEWPROV} zurÃŧckgesetzt.\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Standardanbieter wurden zurÃŧckgesetzt"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Warte auf vollständige CMDR Anmeldung"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Journal-Ordner bereits gesperrt"; @@ -246,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Standard"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -339,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Fehler: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Einstellungen"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Bitte wähle zu speichernde Daten aus"; @@ -360,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Speicherort"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Ordner..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Durchsuchen..."; @@ -376,23 +388,11 @@ "CAPI Settings" = "CAPI-Einstellungen"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Fleet Carrier CAPI-Anfragen aktivieren"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Makro"; +"Enable Fleet Carrier CAPI Queries" = "Carrier-CAPI-Anfragen aktivieren"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Hotkey"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Starte {APP} neu, um den TastenkÃŧrzel nutzen zu kÃļnnen"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} benÃļtigt fÃŧr TastenkÃŧrzel Systemrechte (Bedienungshilfen)"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Öffne Systemeinstellungen"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Nur wenn Elite: Dangerous geÃļffnet ist"; @@ -468,9 +468,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Plugin-Ordner"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Öffnen"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tipp: Du kannst ein Plugin deaktivieren, indem{CR}du '{EXT}' zu seinem Ordnernamen hinzufÃŧgst"; @@ -489,6 +486,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Deaktivierte Plugins"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Fehler im System Profiler"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Kontostand"; @@ -768,3 +768,24 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Schiffe"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} ist verfÃŧgbar"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "Stabil"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Update-Kanal"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Update-Kanal geändert auf {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Update-Kanal von Beta auf Stabil geändert. Du wirst keine weiteren Beta-Updates erhalten. Du wirst bis zum nächsten stabilen Release auf der aktuellen Beta-Version bleiben.\n\nDu kannst manuell zur aktuellen stabilen Version zurÃŧckkehren. Um dies zu tun, musst du die aktuelle stabile Version manuell herunterladen und installieren. Beachte, dass dies Fehler verursachen oder gar nicht funktionieren kÃļnnte, wenn du von einem grÃļßeren Versionssprung mit signifikanten Änderungen downgradest.\n\nMÃļchtest du GitHub Ãļffnen, um den neuesten Release herunterzuladen?"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Kann keinen non-Text-Inhalt einfÃŧgen."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Öffnen in {URL}"; + diff --git a/L10n/en.template b/L10n/en.template index 3aed0952..6e4d6392 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "Error: unable to get token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPI down for maintenance"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Edit"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "View"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Window"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Help"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Check for Updates..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Open System Profiler"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Save Raw Data..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Copy"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Cut"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Paste"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Select All"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "CAPI auth disabled by killswitch"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier disabled by killswitch"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: No fleetcarrier data returned"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: No Fleet Carrier data returned"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Fleetcarrier data incomplete"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Fleet Carrier data incomplete"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: No commander data returned"; @@ -213,12 +225,6 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "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: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ -"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?"; - -/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ -"FDevIDs: Missing Commodity Files" = "FDevIDs: Missing Commodity Files"; - /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Plugins"; @@ -231,6 +237,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name."; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "One or more of your URL Providers were invalid, and have been reset:\r\n\r\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Default Providers Reset"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Awaiting Full CMDR Login"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Journal directory already locked"; @@ -246,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Default"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -339,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Error: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Preferences"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Please choose what data to save"; @@ -360,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "File location"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Change..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Browse..."; @@ -376,23 +388,11 @@ "CAPI Settings" = "CAPI Settings"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Enable Fleetcarrier CAPI Queries"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Keyboard shortcut"; +"Enable Fleet Carrier CAPI Queries" = "Enable Fleet Carrier CAPI Queries"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Hotkey"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Open System Preferences"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app"; @@ -468,8 +468,11 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Plugins folder"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Open"; +/* prefs.py: Label on button used to open the Plugin Folder; */ +"Open Plugins Folder" = "Open Plugins Folder"; + +/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */ +"Plugin Directory Location" = "Plugin Directory Location"; /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "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"; @@ -489,6 +492,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Disabled Plugins"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Error in System Profiler"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Balance"; @@ -789,3 +795,32 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Ships"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} is available"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "Stable"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Update Track"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Update Track Changed to {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?"; + +/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */ +"Restart Required" = "Restart Required"; + +/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */ +"A restart of EDMC is required. EDMC will now restart." = "A restart of EDMC is required. EDMC will now restart."; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Cannot paste non-text content."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Open in {URL}"; + +/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */ +"Copy Inara SLEF" = "Copy Inara SLEF"; diff --git a/L10n/es.strings b/L10n/es.strings index d2cb7b4b..9cdfa007 100644 --- a/L10n/es.strings +++ b/L10n/es.strings @@ -67,12 +67,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Editar"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "VisualizaciÃŗn"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Ventana"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Ayuda"; @@ -184,13 +178,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Por defecto"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -271,9 +265,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Error: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Preferencias"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Seleccione quÊ datos quiere guardar"; @@ -292,9 +283,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "LocalizaciÃŗn de archivos"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Cambiar..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Examinar..."; @@ -304,21 +292,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "LocalizaciÃŗn del archivo de Journal de E:D"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Tecla de acceso directo"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Tecla de acceso directo"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Reinicie {APP} para usar los accesos directos de teclado"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} necesita permisos para usar teclas de acceso directo"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Abrir Preferencias del Sistema"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Solo cuando Elite: Dangerous es la aplicaciÃŗn activa"; @@ -385,9 +361,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Carpeta de Plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Abrir"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Puedes desactivar un plugin aÃąadiendo{CR}'{EXT}' al nombre de su carpeta"; diff --git a/L10n/fi.strings b/L10n/fi.strings index d53d3bc8..2893f2d2 100644 --- a/L10n/fi.strings +++ b/L10n/fi.strings @@ -61,12 +61,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Muokkaa"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Näytä"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Ikkuna"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Apua"; @@ -229,9 +223,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Inaran virhe: {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Valinnat"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Valitse mitkä tiedot tallennetaan"; @@ -250,9 +241,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Tiedostosijainti"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Muuta..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Selaa..."; @@ -262,21 +250,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D lokikirjan tiedostosijainti"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "NäppäimistÃļoikopolku"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Pikanäppäin"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Käynnistä {APP} uudelleen käyttääksesi pikanäppäimiä"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} tarvitsee lupasi pikakuvakkeiden käyttämiseen"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Avaa järjestelmän asetukset"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Vain jos Elite:Dangerous on käynnissä"; @@ -343,9 +319,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Lisäosien hakemistosijainti"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Avaa"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Vihje: Voit poistaa lisäosan käytÃļstä{CR}lisäämällä '{EXT}' sen hakemistonimeen"; diff --git a/L10n/fr.strings b/L10n/fr.strings index 93e09777..3922bbbc 100644 --- a/L10n/fr.strings +++ b/L10n/fr.strings @@ -1,21 +1,59 @@ +/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ +"Send flight log and CMDR status to EDSM" = "Envoyer les donnÊes de vol et le status de CMDR à EDSM"; + +/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ +"Open Log Folder" = "Ouvrir le dossier de log"; + +/* inara.py:Text Inara Show API key; In files: inara.py:305; */ +"Show API Key" = "Afficher la clÊ API"; /* Language name */ "!Language" = "Français"; +/* companion.py: Frontier CAPI didn't respond; In files: companion.py:226; */ +"Error: Frontier CAPI didn't respond" = "L'API Compagnon de Frontier ne rÊpond pas"; + /* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:245; */ "Error: Frontier server is lagging" = "Erreur : Le serveur Frontier ne rÊpond pas"; +/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:261; */ +"Docked but unknown station: EDO Settlement?" = "AmarrÊ, mais station inconnue : colonie EDO ?"; + /* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:271; */ "Error: Invalid Credentials" = "Erreur : Identifiants invalides"; /* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:296; */ "Error: Wrong Cmdr" = "Erreur : Cmdr incorrect"; +/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:432; companion.py:517; */ +"Error" = "Erreur"; + +/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:475; companion.py:480; */ +"Error: Couldn't check token customer_id" = "Erreur : Impossible de vÊrifier le jeton customer_id"; + +/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:486; */ +"Error: customer_id doesn't match!" = "Erreur : customer_id ne correspond pas !"; + +/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ +"Error: unable to get token" = "Erreur : impossible d'obtenir le jeton"; + +/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ +"Frontier CAPI down for maintenance" = "L'API Compagnon de Frontier est en panne pour maintenance"; + +/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:856; */ +"Frontier CAPI query failure" = "Échec de la requÃĒte à l'API Compagnon Frontier"; + /* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */ "Update" = "Mettre à jour"; /* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:710; prefs.py:875; */ "Always on top" = "Toujours visible"; +/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:837; */ +"Unknown" = "Inconnue"; + +/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:906; */ +"Error: Check E:D journal file location" = "Erreur : VÊrifier l'emplacement du fichier de journal Elite : Dangerous"; + /* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */ "Cmdr" = "Cmd"; @@ -40,12 +78,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Édition"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "PrÊsentation"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "FenÃĒtre"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Aide"; @@ -64,6 +96,12 @@ /* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:933; EDMarketConnector.py:953; */ "Documentation" = "Documentation"; +/* EDMarketConnector.py: Help > Troubleshooting; In files: EDMarketConnector.py:934; EDMarketConnector.py:954; */ +"Troubleshooting" = "RÊsolution de problème"; + +/* EDMarketConnector.py: Help > Report A Bug; In files: EDMarketConnector.py:935; EDMarketConnector.py:955; */ +"Report A Bug" = "Signaler un bug"; + /* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:936; EDMarketConnector.py:956; */ "Privacy Policy" = "Politique de ConfidentialitÊ"; @@ -178,9 +216,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Erreur : Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "PrÊfÊrences"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Veuillez choisir les donnÊes à sauvegarder"; @@ -199,9 +234,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Emplacement des fichiers"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "SpÊcifier..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Parcourir..."; @@ -211,21 +243,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "Emplacement du journal E:D"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Raccourci clavier"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Raccourci clavier"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "RÊdÊmarrez {APP} pour utiliser les raccourcis"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} a besoin de permissions pour utiliser les raccourcis"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Ouvrir PrÊfÊrences Système"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Seulement quand Elite: Dangerous est l'application active"; @@ -271,9 +291,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "RÊpertoire des plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Ouvrir"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Astuce : Vous pouvez dÊsactiver un plugin en{CR}ajoutant '{EXT}' à son nom de rÊpertoire"; diff --git a/L10n/hu.strings b/L10n/hu.strings index c3311ac7..face43a5 100644 --- a/L10n/hu.strings +++ b/L10n/hu.strings @@ -37,12 +37,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Szerkeszt"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "NÊzet"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Ablak"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "SegítsÊg"; @@ -169,9 +163,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Hiba:Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "BeÃĄllítÃĄsok"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "KÊrem vÃĄlassza ki a menteni kivÃĄnt adatokat."; @@ -190,9 +181,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "FÃĄlj helye"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "MÃŗdosítÃĄs..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "BÃļngÊsz"; @@ -202,21 +190,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D journal fÃĄjl helye"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "BillentyÅąparancsok"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Gyorsgomb"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "ÚjranidítÃĄs {APP} parancsikonok hasznÃĄlatÃĄval"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} engedÊly szÃŧksÊges a parancsikon hasznÃĄlatÃĄhoz"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "RendszerbeÃĄllítÃĄsok megnyitÃĄss"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Csak ha az Elite Dangerous fut"; @@ -259,9 +235,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Plugin mappa"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Nyisd ki"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tipp:a pluginokat kikapcsolhatod hozzà{CR}adsz a mappa nevÊben egy '{EXT}'"; diff --git a/L10n/it.strings b/L10n/it.strings index a9d11049..4d948883 100644 --- a/L10n/it.strings +++ b/L10n/it.strings @@ -1,3 +1,11 @@ +/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ +"Send flight log and CMDR status to EDSM" = "Invia il registro di volo e lo stato del CMDR a EDSM"; + +/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ +"Open Log Folder" = "Apri la Cartella dei Log"; + +/* inara.py:Text Inara Show API key; In files: inara.py:305; */ +"Show API Key" = "Mostra l'API Key"; /* Language name */ "!Language" = "Italiano"; @@ -28,11 +36,17 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "Errore: impossibile ottenere il token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC ha incontrato un errore critico, e non riesce a risolvere. EDMC si spegnerà in via cautelativa!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Ecco cosa EDMC ha rilevato:\n\n{ERR}\n\nVuoi compilare un Rapporto di Bug su GitHub?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "CAPI di Frontier inattivo per manutenzione"; /* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:856; */ -"Frontier CAPI query failure" = "Errore nella query CAPI di Frontier"; +"Frontier CAPI query failure" = "Query CAPI di Frontier fallita"; /* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */ "Update" = "Recupera"; @@ -70,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Appunti"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Vista"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Finestra"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Guida"; @@ -85,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Controlla aggiornamenti..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Apri Profilazione di Sistema"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Salva i Dati Grezzi..."; @@ -115,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Copia"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Taglia"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Incolla"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Seleziona Tutto"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "Autenticazione CAPI disabilitata dal killswitch"; @@ -157,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "Fleetcarrier CAPI disabilitato dal killswitch"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Nessun dato della fleetcarrier ricevuto"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: nessun dato della Fleet Carrier ottenuto"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Dati della Fleetcarrier incompleti"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: dati della Fleet Carrier incompleti"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Nessun dato sul commandante"; @@ -211,6 +231,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins senza Supporto Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: plugin non funzionanti"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Impossibile caricare uno o piÚ plugin abilitati. Consulta l'elenco nella scheda '{PLUGINS}' di '{FILE}' > '{SETTINGS}'. CiÃ˛ potrebbe essere causato da un'errata struttura delle cartelle. Il file load.py dovrebbe trovarsi in plugins/PLUGIN_NAME/load.py.\n\nPuoi disabilitare un plugin rinominando la sua cartella in modo che abbia '{DISABLED}' alla fine del nome."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Uno o piÚ Provider URL non erano validi e sono stati ripristinati:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} era impostato su {OLDPROV} ed è stato reimpostato su {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Reset dei provider predefiniti"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "In attesa del login completo al CMDR"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "cartella Journal già bloccata"; @@ -226,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Predefinito"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normale"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -319,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Errore: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Preferenze"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Si prega di scegliere che dati salvare"; @@ -340,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Percorso del file"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Cambia..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Sfoglia..."; @@ -356,23 +388,11 @@ "CAPI Settings" = "Impostazioni CAPI"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Abilita le query CAPI della Fleetcarrier"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Scorciatoia di tastiera"; +"Enable Fleet Carrier CAPI Queries" = "Abilita le query CAPI per le Fleet Carrier"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Hotkey"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Riavvia {APP} per usare le scorciatoie"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} ha bisogno dei permessi per usare le scorciatoie"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Apri Preferenze di Sistema"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Solo quando “Elite: Dangerous” è in primo piano"; @@ -448,8 +468,11 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Cartella dei plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Sfoglia"; +/* prefs.py: Label on button used to open the Plugin Folder; */ +"Open Plugins Folder" = "Apri la cartella dei plugin"; + +/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */ +"Plugin Directory Location" = "Posizione della Cartella del Plugin"; /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Suggerimento: è possibile disabilitare un plugin {CR}aggiungendo '{EXT}' nel nome della sua cartella"; @@ -463,9 +486,15 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "Informazioni sulla migrazione dei plugins"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Plugin non funzionanti"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Plugin disabilitati"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Errore nel Sistema di Profilazione"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Saldo"; @@ -766,3 +795,32 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Navi"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} è disponibile"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "Stabile"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Canale di aggiornamento"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Canale di aggiornamento cambiato in {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Il canale di aggiornamento è cambiato da Beta a Stabile. Non riceverai piÚ aggiornamenti Beta. Rimarrai sulla tua attuale versione Beta fino alla prossima release Stabile.\n\nPuoi tornare manualmente all'ultima versione Stabile. Per farlo, devi scaricare e installare manualmente l'ultima versione Stabile. Nota che questo potrebbe introdurre bug o causare malfunzionamenti se si esegue il downgrade tra versioni principali con cambiamenti significativi.\n\nVuoi aprire GitHub per scaricare l'ultima release?"; + +/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */ +"Restart Required" = "Riavvio necessario"; + +/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */ +"A restart of EDMC is required. EDMC will now restart." = "È necessario riavviare EDMC. EDMC si riavvierà ora."; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Non si puÃ˛ incollare contenuto non testuale"; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Apri {URL}"; + +/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */ +"Copy Inara SLEF" = "Copia Inara SLEF"; diff --git a/L10n/ja.strings b/L10n/ja.strings index 68a9fd34..9ef405c1 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "エナãƒŧ: トãƒŧã‚¯ãƒŗãŒå–åž—ã§ããžã›ã‚“"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMCã¯č‡´å‘Ŋįš„ãĒå•éĄŒãĢ遭遇し、垊旧できぞせん。EDMCはč‡ĒčēĢぎäŋč­ˇãŽãŸã‚ã‚ˇãƒŖãƒƒãƒˆãƒ€ã‚Ļãƒŗã—ãžã™īŧ"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "EDMCが検įŸĨã—ãŸå•éĄŒã¯äģĨ下ぎ通りです:\n\n{ERR}\n\nGitHubãĢバグãƒŦポãƒŧトをäŊœæˆã—ぞすか?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPIãŒãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãŽãŸã‚åœæ­ĸ中"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "ᎍ集"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "襨į¤ē"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "ã‚Ļã‚Ŗãƒŗãƒ‰ã‚Ļ"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "ヘãƒĢプ"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "更新ぎįĸēčĒ..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "ã‚ˇã‚šãƒ†ãƒ ãƒ—ãƒ­ãƒ•ã‚Ąã‚¤ãƒŠãƒŧを開く"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "į”Ÿãƒ‡ãƒŧã‚ŋぎäŋå­˜..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "ã‚ŗãƒ”ãƒŧ"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "切り取り"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "č˛ŧりäģ˜ã‘"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "全ãĻを選択"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "CAPIčĒč¨ŧはkillswitchãĢã‚ˆãŖãĻį„ĄåŠšãĢされãĻいぞす"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPIフãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸはkillswitchãĢã‚ˆãŖãĻį„ĄåŠšãĢされãĻいぞす"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸデãƒŧã‚ŋぎčŋ”äŋĄãĒし"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸデãƒŧã‚ŋぎčŋ”äŋĄãĒし"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸデãƒŧã‚ŋが不厌全"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸデãƒŧã‚ŋが不厌全"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: ã‚ŗãƒžãƒŗãƒ€ãƒŧぎデãƒŧã‚ŋがčŋ”ãŖãĻきぞせんでした"; @@ -213,12 +225,6 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "有劚ãĢしãĻいる1つäģĨä¸ŠãŽãƒ—ãƒŠã‚°ã‚¤ãƒŗãŒPython 3.xをã‚ĩポãƒŧトしãĻいぞせん。'{FILE}' > '{SETTINGS}'\nãƒĄãƒ‹ãƒĨãƒŧã§čĄ¨į¤ēã•ã‚Œã‚‹č¨­åŽšãƒ€ã‚¤ã‚ĸログぎ'{PLUGINS}' ã‚ŋブぎ一čĻ§ã‚’įĸēčĒã—ãĻください。更新済ãŋぎバãƒŧã‚¸ãƒ§ãƒŗãŒã‚ã‚‹ã‹ã‚’įĸēčĒã—ã€ãĒければ開į™ē者ãĢPython 3.xãĢ寞åŋœã™ã‚‹ã‚ˆã†ãĢ開į™ē者ãĢ逪įĩĄã—ãĻください。\n\nãƒ—ãƒŠã‚°ã‚¤ãƒŗã‚’į„ĄåŠšãĢするãĢはフりãƒĢダ名ぎ最垌ãĢ'{DISABLED}'をčŋŊ加しãĻください。"; -/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ -"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevIDãƒ•ã‚Ąã‚¤ãƒĢがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“īŧå•†å“ãĢé–ĸするいくつかぎ抟čƒŊãŒį„ĄåŠšãĢãĒるかもしれぞせん。\n\nã‚ĩブãƒĸジãƒĨãƒŧãƒĢãŽč¨­åŽšæ–šæŗ•ãĢついãĻぎWikiペãƒŧジを開きぞすかīŧŸ"; - -/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ -"FDevIDs: Missing Commodity Files" = "FDevIDs: å•†å“ãƒ•ã‚Ąã‚¤ãƒĢがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“"; - /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "ãƒ—ãƒŠã‚°ã‚¤ãƒŗ"; @@ -231,6 +237,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "有劚ãĢしãĻいる1つäģĨä¸ŠãŽãƒ—ãƒŠã‚°ã‚¤ãƒŗãŒãƒ­ãƒŧドãĢå¤ąæ•—ã—ãžã—ãŸã€‚'{FILE}' > '{SETTINGS}' ãƒĄãƒ‹ãƒĨãƒŧã§čĄ¨į¤ēã•ã‚Œã‚‹č¨­åŽšãƒ€ã‚¤ã‚ĸログぎ'{PLUGINS}' ã‚ŋブぎ一čĻ§ã‚’įĸēčĒã—ãĻãã ã•ã„ã€‚ã“ãŽå•éĄŒã¯čĒ¤ãŖãŸãƒ•ã‚ŠãƒĢダ構造ãĢã‚ˆãŖãĻåŧ•きčĩˇã“されぞす。load.pyãƒ•ã‚Ąã‚¤ãƒĢはplugins/ãƒ—ãƒŠã‚°ã‚¤ãƒŗå/plug-in.pyとしãĻ配įŊŽã•れるåŋ…čĻãŒã‚ã‚Šãžã™ã€‚\n\nãƒ—ãƒŠã‚°ã‚¤ãƒŗã‚’į„ĄåŠšãĢするãĢはフりãƒĢダ名ぎ最垌ãĢ'{DISABLED}'をčŋŊ加しãĻください。"; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "一つäģĨ上ぎURLãƒ—ãƒ­ãƒã‚¤ãƒ€ãŒį„ĄåŠšã§ã‚ãŖãŸãŸã‚ãƒĒã‚ģットされぞした:\n\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} は {OLDPROV} ãĢč¨­åŽšã•ã‚Œã€ {NEWPROV} ãĢãƒĒã‚ģットされぞした\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: デフりãƒĢトプロバイダãƒŧぎãƒĒã‚ģット"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "厌全ãĒCMDRãƒ­ã‚°ã‚¤ãƒŗã‚’åž…æŠŸã—ãĻいぞす"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "ã‚¸ãƒŖãƒŧナãƒĢãƒ‡ã‚ŖãƒŦクトãƒĒはæ—ĸãĢロックされãĻいぞす"; @@ -246,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "デフりãƒĢト"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "č‡Ē動"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "é€šå¸¸į‰ˆ"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "ベãƒŧã‚ŋį‰ˆ"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -339,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "エナãƒŧ: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "į’°åĸƒč¨­åޚ"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "äŋå­˜ã™ã‚‹ãƒ‡ãƒŧã‚ŋãŽį¨ŽéĄžã‚’é¸æŠžã—ãĻ下さい"; @@ -360,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "ãƒ•ã‚Ąã‚¤ãƒĢぎå‡ē力先"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "変更..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "å‚į…§..."; @@ -376,23 +388,11 @@ "CAPI Settings" = "CAPIč¨­åŽš"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸCAPIクエãƒĒを有劚ãĢする"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "ã‚ˇãƒ§ãƒŧトã‚Ģット"; +"Enable Fleet Carrier CAPI Queries" = "フãƒĒãƒŧãƒˆã‚­ãƒŖãƒĒã‚ĸCAPIクエãƒĒを有劚ãĢする"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "ホットキãƒŧ"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "ã‚ˇãƒ§ãƒŧトã‚Ģãƒƒãƒˆã‚’åˆŠį”¨ã™ã‚‹ãĢは {APP} を再čĩˇå‹•しãĻください"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} ãŒã‚ˇãƒ§ãƒŧトã‚Ģãƒƒãƒˆã‚’åˆŠį”¨ã™ã‚‹ãĢã¯č¨ąå¯ãŒåŋ…čĻã§ã™"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "ã‚ˇã‚šãƒ†ãƒ į’°åĸƒč¨­åŽšã‚’é–‹ã"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Elite: Dangerousがã‚ĸã‚¯ãƒ†ã‚Ŗãƒ–ãŽæ™‚ã ã‘æœ‰åŠš"; @@ -468,9 +468,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "ãƒ—ãƒŠã‚°ã‚¤ãƒŗãƒ•ã‚ŠãƒĢダ"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "開く"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "ãƒ’ãƒŗãƒˆ: ãƒ—ãƒŠã‚°ã‚¤ãƒŗã‚’į„ĄåŠšãĢするãĢは{CR}フりãƒĢダ名ãĢ '{EXT}' をčŋŊ加しãĻください"; @@ -489,6 +486,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "į„ĄåŠšãĒãƒ—ãƒŠã‚°ã‚¤ãƒŗ"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "ã‚ˇã‚šãƒ†ãƒ ãƒ—ãƒ­ãƒ•ã‚Ąã‚¤ãƒŠãƒŧでエナãƒŧ"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "åŖå秿ދé̘"; @@ -789,3 +789,24 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "æ‰€æœ‰čˆš"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} がありぞす"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "åŽ‰åŽšį‰ˆ"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "ã‚ĸップデãƒŧトトナック"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "ã‚ĸップデãƒŧトトナックを {TRACK} ãĢ変更しぞす"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "ã‚ĸップデãƒŧトトナックをベãƒŧã‚ŋã‹ã‚‰åŽ‰åŽšį‰ˆãĢ変更しぞした。äģŠåžŒãƒ™ãƒŧã‚ŋį‰ˆãŽã‚ĸップデãƒŧトを受け取ることはありぞせん。æŦĄãŽåŽ‰åŽšį‰ˆãŒãƒĒãƒĒãƒŧ゚されるぞでこぎベãƒŧã‚ŋį‰ˆã‚’ã”åˆŠį”¨ã„ãŸã ã‘ãžã™ã€‚\n\næ‰‹å‹•ã§æœ€æ–°ãŽåŽ‰åŽšį‰ˆãĢæˆģすことができぞす。そぎためãĢã¯ã€æœ€æ–°ãŽåŽ‰åŽšį‰ˆã‚’æ‰‹å‹•ã§ãƒ€ã‚Ļãƒŗãƒ­ãƒŧドしãĻã‚¤ãƒŗã‚šãƒˆãƒŧãƒĢするåŋ…čĻãŒã‚ã‚Šãžã™ã€‚å¤§ããĒ変更をäŧ´ã†ãƒĄã‚¸ãƒŖãƒŧバãƒŧã‚¸ãƒ§ãƒŗé–“ãŽãƒ€ã‚Ļãƒŗã‚°ãƒŦãƒŧドぎ場合、バグがį™ēį”Ÿã—ãŸã‚Šã€åŽŒå…¨ãĢåŖŠã‚ŒãŸã‚Šã™ã‚‹å¯čƒŊ性があることãĢæŗ¨æ„ã—ãĻください。\n\nGitHubを開いãĻ最新ãƒĒãƒĒãƒŧ゚をダã‚Ļãƒŗãƒ­ãƒŧドしぞすかīŧŸ"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "テキ゚トではãĒい内厚はč˛ŧりäģ˜ã‘できぞせん"; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "{URL} で開く"; + diff --git a/L10n/ko.strings b/L10n/ko.strings index 577695b0..63f4c613 100644 --- a/L10n/ko.strings +++ b/L10n/ko.strings @@ -70,12 +70,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "íŽ¸ė§‘"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "ëŗ´ę¸°"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "ė°Ŋ"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "ë„ė›€ë§"; @@ -202,13 +196,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "ę¸°ëŗ¸ę°’"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "ėžë™"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "ėŧ반"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "베타"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -289,9 +283,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "똤ëĨ˜: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "네렕"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "ė–´ëŠ ë°ė´í„°ëĨŧ ė €ėžĨ할 ė§€ ė„ íƒí•´ėŖŧė„¸ėš”"; @@ -310,9 +301,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "파ėŧ ėœ„ėš˜"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "바꾸기..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "ė°žė•„ëŗ´ę¸°..."; @@ -322,21 +310,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D ė €ë„(journal) 파ėŧ ėœ„ėš˜"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "단ėļ•키 네렕"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "단ėļ•키"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "단ėļ•키ëĨŧ ė‚ŦėšŠí•˜ë ¤ëŠ´ {APP}ė„(ëĨŧ) ë‹¤ė‹œ ė‹œėž‘í•˜ė‹­ė‹œė˜¤"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "단ėļ•기 ė‚ŦėšŠė„ ėœ„í•´ė„  {APP}ė´ ęļŒí•œ í—ˆėšŠė„ í•„ėš”ëĄœ 합니다"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "ė‹œėŠ¤í…œ 네렕 뗴揰"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Elite: Dangerous가 í™œė„ą ė°Ŋ ėƒíƒœėŧ때만"; @@ -412,9 +388,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "플ëŸŦęˇ¸ė¸ 폴더"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "뗴揰"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "팁: 플ëŸŦęˇ¸ė¸ė„ ëš„í™œė„ąí™”í•˜ë ¤ëŠ´{CR} '{EXT}'ëĨŧ 폴더ëĒ… ë’¤ė— ėļ”ę°€í•˜ëŠ´ 됨"; diff --git a/L10n/lv.strings b/L10n/lv.strings index 3aa46284..c89e16de 100644 --- a/L10n/lv.strings +++ b/L10n/lv.strings @@ -34,12 +34,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Labot"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Skats"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Logs"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "PalÄĢdzÄĢba"; @@ -157,9 +151,6 @@ /* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:953; edsm.py:1043; */ "Error: Can't connect to EDSM" = "KÄŧÅĢda: Nevar savienoties ar EDSM"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "IestatÄĢjumi"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "LÅĢdzu izvēlaties kādus datus saglabāt"; @@ -178,9 +169,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Faila atraÅĄanās vieta"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "MainÄĢt..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "PārlÅĢkot..."; @@ -190,21 +178,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D Åžurnāla faila atraÅĄanās vieta"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Ātrais taustiÅ†ÅĄ"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Ātrais taustiÅ†ÅĄ"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Restartējiet {APP} lai izmantotu ÄĢssceÄŧus"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} vajag atÄŧaujas lai izmantotu ÄĢssceÄŧus"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Atvērt sistēmas iestatÄĢjumus"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Tikai kad Elite: Dangerous ir aktÄĢvā aplikācija"; @@ -244,9 +220,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Spraudņu mape"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Atvērt"; - /* prefs.py: Label on list of enabled plugins; In files: prefs.py:934; */ "Enabled Plugins" = "Iespējoti Spraudņi"; diff --git a/L10n/nl.strings b/L10n/nl.strings index f6a4a32f..677236bc 100644 --- a/L10n/nl.strings +++ b/L10n/nl.strings @@ -1,3 +1,11 @@ +/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ +"Send flight log and CMDR status to EDSM" = "Stuur logboek en CMDR-status naar EDSM"; + +/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ +"Open Log Folder" = "Open logboekmap"; + +/* inara.py:Text Inara Show API key; In files: inara.py:305; */ +"Show API Key" = "Toon API key"; /* Language name */ "!Language" = "Nederlands"; @@ -10,12 +18,33 @@ /* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:296; */ "Error: Wrong Cmdr" = "Fout: Verkeerde Cmdr"; +/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:432; companion.py:517; */ +"Error" = "Fout"; + +/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:486; */ +"Error: customer_id doesn't match!" = "Fout: customer_id komt niet overeen!"; + +/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ +"Error: unable to get token" = "Fout: kon token niet verkrijgen"; + +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "Er trad een kritieke fout op, en EDMC kan niet herstellen. Het programma sluit af uit zelfbescherming!"; + +/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ +"Frontier CAPI down for maintenance" = "Frontier CAPI offline voor onderhoud"; + /* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */ "Update" = "Bijwerken"; /* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:710; prefs.py:875; */ "Always on top" = "Altijd op voorgrond"; +/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:837; */ +"Unknown" = "Onbekend"; + +/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:906; */ +"Error: Check E:D journal file location" = "Fout: Controleer locatie van E:D logboekbestand"; + /* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */ "Cmdr" = "Cmdr"; @@ -37,12 +66,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Bewerken"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Beeld"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Venster"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Help"; @@ -52,6 +75,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Controleren op updates..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Open systeemprofiler"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Sla onbewerkte gegevens op..."; @@ -61,6 +87,12 @@ /* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:933; EDMarketConnector.py:953; */ "Documentation" = "Documentatie"; +/* EDMarketConnector.py: Help > Troubleshooting; In files: EDMarketConnector.py:934; EDMarketConnector.py:954; */ +"Troubleshooting" = "Probleemoplossing"; + +/* EDMarketConnector.py: Help > Report A Bug; In files: EDMarketConnector.py:935; EDMarketConnector.py:955; */ +"Report A Bug" = "Meld een bug"; + /* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:936; EDMarketConnector.py:956; */ "Privacy Policy" = "Privacybeleid"; @@ -76,6 +108,18 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "KopieÃĢren"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Knippen"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Plakken"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Selecteer alles"; + +/* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ +"CAPI auth disabled by killswitch" = "CAPI authenticatie uitgeschakeld door killswitch"; + /* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:978; */ "Logging in..." = "Bezig met inloggen..."; @@ -91,9 +135,33 @@ /* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:1038; */ "Station doesn't have a market!" = "Station heeft geen markt!"; +/* EDMarketConnector.py: CAPI queries aborted because Cmdr name is unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because Cmdr name is unknown; In files: EDMarketConnector.py:1077; EDMarketConnector.py:1164; */ +"CAPI query aborted: Cmdr name unknown" = "CAPI query onderbroken: Cmdr naam onbekend"; + +/* EDMarketConnector.py: CAPI queries aborted because game mode unknown; In files: EDMarketConnector.py:1083; */ +"CAPI query aborted: Game mode unknown" = "CAPI query onderbroken: game mode onbekend"; + +/* EDMarketConnector.py: CAPI queries aborted because GameVersion unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because GameVersion unknown; In files: EDMarketConnector.py:1089; EDMarketConnector.py:1170; */ +"CAPI query aborted: GameVersion unknown" = "CAPI query onderbroken: GameVersion onbekend"; + +/* EDMarketConnector.py: CAPI queries aborted because current star system name unknown; In files: EDMarketConnector.py:1095; */ +"CAPI query aborted: Current system unknown" = "CAPI query onderbroken: huidig systeem onbekend"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in multi-crew on other Cmdr's ship; In files: EDMarketConnector.py:1101; */ +"CAPI query aborted: In other-ship multi-crew" = "CAPI query onderbroken: in multi-crew ander schip"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:1107; */ +"CAPI query aborted: CQC (Arena) detected" = "CAPI query onderbroken: in CQC (Arena)"; + /* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; In files: EDMarketConnector.py:1128; EDMarketConnector.py:1179; */ "Fetching data..." = "Data wordt opgehaald..."; +/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ +"CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier uitgeschakeld door killswitch"; + +/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ +"CAPI: No commander data returned" = "CAPI: geen commander data verkregen"; + /* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:1246; stats.py:333; */ "Who are you?!" = "Wie ben je?!"; @@ -103,6 +171,12 @@ /* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1259; stats.py:349; */ "What are you flying?!" = "Waar vlieg je in?!"; +/* EDMarketConnector.py: Frontier CAPI server error when fetching data; In files: EDMarketConnector.py:1384; */ +"Frontier CAPI server error" = "Frontier CAPI serverfout"; + +/* EDMarketConnector.py: Frontier CAPI Access Token expired, trying to get a new one; In files: EDMarketConnector.py:1390; */ +"CAPI: Refreshing access token..." = "CAPI: toegangstoken vernieuwen..."; + /* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1434; */ "Last updated at %H:%M:%S" = "Voor het laatst bijgewerkt om %H:%M:%S"; @@ -121,6 +195,9 @@ /* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1864; prefs.py:292; */ "OK" = "OK"; +/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1936; */ +"Shutting down..." = "Afsluiten..."; + /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Een of meer ingeschakelde plugins hebben nog geen Python 3.x ondersteuning. Kijk op de lijst op de '{PLUGINS}' tab van '{FILE}' > '{SETTINGS}'. Check of er een bijgewerkte versie beschikbaar is. Laat anders de developer weten dat ze de code moeten updaten voor Python 3.x.\n\nJe kan de plugin uitschakelen door het mapje te hernoemen met '{DISABLED}' aan het eind van de naam."; @@ -130,9 +207,45 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMS: Plugins zonder Python 3.x ondersteuning"; +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Een of meer van je ingeschakelde plugins kon niet laden. Controleer de lijst op de '{PLUGINS}' tab van '{FILE}' > '{SETTINGS}'. Dit zou veroorzaakt kunnen worden door een verkeerde mappenstructuur. Het bestand load.py hoort op plugins/PLUGIN_NAAM/load.py\n\nJe kunt een plugin uitschakelen door '{DISABLED}' aan het einde van de naam van de plugin-map toe te voegen."; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Wachten op volledige CMDR login"; + +/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ +"Journal directory already locked" = "Logboekmap al vergrendeld"; + +/* journal_lock.py: Text for when newly selected Journal directory is already locked; In files: journal_lock.py:225:226; */ +"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "De nieuwe logboekmaplocatie is al vergrendeld. {CR} Je kan dit probleem verhelpen en het dan opnieuw proberen, of dit probleem negeren."; + +/* journal_lock.py: Generic 'Retry' button label; In files: journal_lock.py:230; */ +"Retry" = "Probeer opnieuw"; + +/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:234; */ +"Ignore" = "Negeer"; + /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Standaard"; +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ +"Auto" = "Auto"; + +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ +"Normal" = "Normaal"; + +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ +"Beta" = "Beta"; + +/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: coriolis.py:97; */ +"Normal URL" = "Normale URL"; + +/* coriolis.py: Generic 'Reset' button label; In files: coriolis.py:100; coriolis.py:109; */ +"Reset" = "Reset"; + +/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: coriolis.py:106; */ +"Beta URL" = "Beta-URL"; + /* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:458; eddn.py:2413; eddn.py:2451; eddn.py:2519; */ "Error: Can't connect to EDDN" = "Fout: Kan geen verbinding maken met EDDN"; @@ -175,9 +288,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Fout: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Preferences"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Welke gegevens wilt u opslaan?"; @@ -196,9 +306,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Bestand locatie"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Wijzigen..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Bladeren..."; @@ -208,21 +315,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D journaal bestand locatie"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Toetscombinaties"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Hotkey"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "{APP} heropstarten om toetscombinaties te gebruiken"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} heeft toestemming nodig om toetscombinaties te gebruiken"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Open Systeemvoorkeuren"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Alleen wanneer Elite: Dangerous de actieve applicatie is"; @@ -271,9 +366,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Plugins map"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Openen"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: Je kan een plugin deactiveren door '{EXT}'{CR}toe te voegen aan de map naam"; @@ -289,6 +381,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Gedeactiveerde plugins"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Fout in systeemprofiler"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Balans"; diff --git a/L10n/pl.strings b/L10n/pl.strings index 72a66718..dd092529 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -78,12 +78,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Edycja"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Widok"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Okno"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Pomoc"; @@ -165,12 +159,6 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "Fleetcarier CAPI wyłączony \"kill switchem\""; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Nie zwrÃŗcono danych dotyczących lotniskowca"; - -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Niekompletne dane lotniskowca"; - /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Nie zwrÃŗcono danych dowÃŗdcy"; @@ -219,6 +207,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Pluginy Nie Wspierające Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Uszkodzone pluginy"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jeden lub więcej plugin nie został załadowany. SprawdÅē listę na zakładce '{PLUGINS}' w menu '{FILE}' > '{SETTINGS}'. MoÅŧe to być spowodowane błedną strukturą katalogÃŗw. Plik load.py powinien być umiezczony w plugins/NAZWA PLUGINA/load.py.\n\nMoÅŧesz wyłączyć plugin zmieniając nazwę jego folderu tak, aby zawierała '{DISABLED}' na końcu nazwy."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Jeden lub więcej adres URL jest błędny i został zresetowany:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} ustawiony na {OLDPROV} i został zresetowany do {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Dostawca domyślny zresetowany"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Oczekiwanie na pełne zalogowanie do gry"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Katalog dziennika zablokowany"; @@ -234,13 +240,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Domyślne"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normalny"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -327,9 +333,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Błąd: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Preferencje"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Jakie dane zapisywać"; @@ -348,9 +351,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "PołoÅŧenie pliku"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Zmień..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Przeglądaj..."; @@ -363,24 +363,9 @@ /* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */ "CAPI Settings" = "Ustawienia CAPI"; -/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Włącz zapytania CAPI dla lotniskowca"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "SkrÃŗt klawiaturowy"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Skr. Klaw."; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Zrestartuj {APP} by uÅŧyć{CR}skrÃŗtu klawiszowego."; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} wymaga uprawnień by{CR}mÃŗc korzystać ze skrÃŗtÃŗw klawiszowych."; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "OtwÃŗrz Preferencje systemowe"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Tylko gdy Elite: Dangerous jest aplikacją aktywną"; @@ -456,9 +441,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Folder pluginÃŗw"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "OtwÃŗrz"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "WskazÃŗwka: MoÅŧesz wyłączyć pluginy{CR}dodając '{EXT}' do nazwy folderu"; @@ -471,6 +453,9 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "Informacja o migracji pluginÃŗw"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Niedziałające pluginy"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Pluginy wyłączone"; @@ -774,3 +759,6 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Statki"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "Dostępna nowa wersja {NEWVER}"; + diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings index e719101a..187a0627 100644 --- a/L10n/pt-BR.strings +++ b/L10n/pt-BR.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "Erro: nÃŖo foi possível obter o token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC encontrou um erro crítico e nÃŖo conseguiu recuperar-se. Encerrando o EDMC para proteÃ§ÃŖo prÃŗpria!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Aqui estÃĄ o que o EDMC detectou:\n\n{ERR}\n\nGostaria de reportar o bug em nosso GitHub?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "CAPI da Frontier indisponível por manutenÃ§ÃŖo"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Editar"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Visualizar"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Janela"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Ajuda"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Verificar por AtualizaçÃĩes..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Abrir Perfil de Sistema"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Salvar dados brutos..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Copiar"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Recortar"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Colar"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Selecionar tudo"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "AutenticaÃ§ÃŖo CAPI desativada pelo botÃŖo de interrupÃ§ÃŖo."; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI para porta-frotas desativado pelo botÃŖo de interrupÃ§ÃŖo."; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Nenhum dado de porta-fortas retornado"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: Nenhum dado de porta-frotas retornado"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Dados de porta-frota incompletos"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de porta-frotas incompletos"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Nenhum dado de comandante retornado"; @@ -219,6 +231,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins sem Suporte ao Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Plugins Quebrados"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Um ou mais plugins ativados falhou ao carregar. Verifique a lista na aba '{PLUGINS}', em '{FILE}' > '{SETTINGS}'. Isto pode ter ocorrido por um erro na estrutura de pastas. O arquivo load.py deve estar localizado em plugins/NOME_PLUGIN/load.py.\n\nVocÃĒ pode desativar plugins renomeando a pasta para ter '.{DISABLED}' ao final do nome."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Um ou mais dos seus Provedores de URL eram invÃĄlidos e portanto foram reconfigurados:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} estava como {OLDPROV}, e foi reconfigurado para {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Provedores PadrÃŖo Reconfigurados"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Aguardando CMDT entrar no jogo"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "DiretÃŗrio de Jornais jÃĄ estÃĄ bloqueado"; @@ -234,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "PadrÃŖo"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "AutomÃĄtico"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -327,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Erro: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "PreferÃĒncias"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Por favor, escolha quais dados para salvar"; @@ -340,7 +367,7 @@ "Market data in Trade Dangerous format file" = "Dados de mercado no arquivo de formato Trade Dangerous"; /* prefs.py: Settings > Output option; In files: prefs.py:360; */ -"Ship loadout" = "ModulaÃ§ÃŖo da nave"; +"Ship loadout" = "Build da Nave"; /* prefs.py: Settings > Output option; In files: prefs.py:370; */ "Automatically update on docking" = "Atualizar automaticamente na docagem"; @@ -348,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "LocalizaÃ§ÃŖo do Arquivo"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Alterar..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Procurar..."; @@ -364,23 +388,11 @@ "CAPI Settings" = "ConfiguraçÃĩes de CAPI"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Ativar requisiçÃĩes CAPI para porta-frotas"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Atalho de teclado"; +"Enable Fleet Carrier CAPI Queries" = "Ativar requisiçÃĩes CAPI para porta-frotas"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Tecla de atalho"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Reinicie o {APP} para usar os atalhos"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} precisa de permissÃŖo para usar atalhos"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Abrir preferÃĒncias do sistema"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Somente quando o Elite: Dangerous Ê o aplicativo ativo"; @@ -456,8 +468,11 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Pasta dos plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Abrir"; +/* prefs.py: Label on button used to open the Plugin Folder; */ +"Open Plugins Folder" = "Abrir pasta dos plugins"; + +/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */ +"Plugin Directory Location" = "LocalizaÃ§ÃŖo da pasta dos plugins"; /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Dica: VocÃĒ pode desativar um plugin{CR}adicionando '{EXT}' ao nome da pasta"; @@ -471,9 +486,15 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "InformaçÃĩes de migraÃ§ÃŖo de plugins"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Plugins Quebrados"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Plugins desabilitados"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Erro no Perfil de Sistema"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Balanço"; @@ -774,3 +795,32 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Naves"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} estÃĄ disponível"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "EstÃĄvel"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Caminho de AtualizaçÃĩes"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Caminho de AtualizaçÃĩes modificado para {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Caminho de atualizaçÃĩes modificado de EstÃĄvel para Beta. VocÃĒ nÃŖo receberÃĄ mais atualizaçÃĩes Beta. VocÃĒ seguirÃĄ na versÃŖo Beta atual atÊ a prÃŗxima atualizaÃ§ÃŖo EstÃĄvel.\n\nVocÃĒ pode reverter manualmente para a Ãēltima versÃŖo EstÃĄvel. Para fazer isso, faça download e instale a Ãēltima versÃŖo EstÃĄvel manualmente. Fique atento: isto pode introduzir bugs ou quebrar completamente a aplicaÃ§ÃŖo caso vocÃĒ o retorno volte para uma versÃŖo principal com mudanças significativas.\n\nGostaria de abrir o GitHub para fazer download da Ãēltima versÃŖo?"; + +/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */ +"Restart Required" = "ReinicializaÃ§ÃŖo necessÃĄria"; + +/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */ +"A restart of EDMC is required. EDMC will now restart." = "É necessÃĄrio reinicializar o EDMC. Isso serÃĄ realizado agora."; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "SÃŗ Ê possível colar texto."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Abrir em {URL}"; + +/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */ +"Copy Inara SLEF" = "Copiar formato SLEF"; diff --git a/L10n/pt-PT.strings b/L10n/pt-PT.strings index ea292b14..c9834977 100644 --- a/L10n/pt-PT.strings +++ b/L10n/pt-PT.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "Erro: NÃŖo Ê possível obter o token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "O EDMC encontrou um erro crítico e nÃŖo consegue recuperar. O EDMC irÃĄ desligar-se para proteÃ§ÃŖo prÃŗpria."; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "O EDMC detectou o seguinte:\n\n{ERR}\n\nDeseja preencher um Bug Report no GitHub?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "CAPI da Frontier em manutenÃ§ÃŖo."; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Editar"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Ver"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Janela"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Ajuda"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Procurar por ActualizaçÃĩes..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Abrir InformaçÃĩes do Sistema"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Salvar dados em bruto..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Copiar"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Cortar"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Colar"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Selecionar Tudo"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "AutenticaÃ§ÃŖo CAPI desligada por botÃŖo"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "Consulta à CAPI de Transportador de Frota cancelada por botÃŖo"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Sem dados de Transportador de Frota"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: Sem dados de Transportador de Frota"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Dados de Transportador de Frota incompletos"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de Transportador de Frota incompletos"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Dados do Comandante nÃŖo recebidos"; @@ -213,12 +225,6 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Um ou mais dos plugins activados nÃŖo possui ainda suporte para Python 3.x. Por favor verifique a lista na aba '{PLUGINS}' em '{FILE}' > '{SETTINGS}'. DeverÃĄ verificar se estÃĄ disponível uma versÃŖo mais actualizada, caso contrÃĄrio, informe o desenvolvedor de que necessita de actualizar o cÃŗdigo para suportar Python 3.x.\n\nPode desactivar um plugin colocando '{DISABLED}' no fim do nome da pasta correspondente."; -/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ -"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "Ficheiros FDevID nÃŖo encontrados. Algumas funcionalidades relacionadas com mercadorias poderÃŖo estar indisponíveis.\nDeseja abrir a pÃĄgina da Wiki sobre configuraÃ§ÃŖo de submÃŗdulos?"; - -/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ -"FDevIDs: Missing Commodity Files" = "FDevIDs: Ficheiros de Mercadorias em falta."; - /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Plugins"; @@ -231,6 +237,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Um ou mais dos plugins activados nÃŖo arrancou correctamente. Por favor consule a lista na aba {PLUGINS}', em '{FILE}' > '{SETTINGS}'. Isto poderÃĄ ser causado por mÃĄ estrutura de pastas. O ficheiro load.py deverÃĄ estar dentro de plugins/NOME_DO_PLUGIN/load.py. \nPode desactivar um plugin colocando '{DISABLED}' no fim do nome da pasta."; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Um ou vÃĄrios dos Provedores de URLs eram invÃĄlidos e foram reconfigurados.\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} era {OLDPROV}, foi reconfigurado para {NEWPROV}"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Provedores for defeito reconfigurados."; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Aguardando pelo login completo do CMDR."; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "DirectÃŗrio do DiÃĄrio jÃĄ se encontra bloqueado"; @@ -246,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Predefinido"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -339,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Erro: Inara: {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "PreferÃĒncias"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Por favor escolher quais os dados a guardar"; @@ -360,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "LocalizaÃ§ÃŖo do Ficheiro"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Alterar..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Procurar..."; @@ -376,23 +388,11 @@ "CAPI Settings" = "DefiniçÃĩes CAPI"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Ligar Consultas CAPI de Transportador de Frota"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Atalho de Teclado"; +"Enable Fleet Carrier CAPI Queries" = "Ligar Consultas CAPI de Transportador de Frota"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Tecla RÃĄpida"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Reinicie o {APP} para usar os atalhos"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "A {APP} precisa de autorizaÃ§ÃŖo para usar atalhos"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Abrir PreferÃĒncias do Sistema"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Apenas quando o Elite:Dangerous Ê a aplicaÃ§ÃŖo activa"; @@ -468,9 +468,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Pasta dos Plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Abrir"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Dica: Pode desactivar um plugin{CR}ao adicionar '{EXT}' ao nome da sua pasta"; @@ -489,6 +486,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Plugins Desactivados"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Erro nas InformaçÃĩes do Sistema"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Balanço"; @@ -789,3 +789,24 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Naves"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} disponível."; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "EstÃĄvel"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Caminho de ActualizaçÃĩes"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Caminho de ActualizaçÃĩes alterado para {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Caminho de ActualizaçÃĩes alterado de EstÃĄvel para Beta. DeixarÃĄ de receber actualizaçÃĩes Beta. IrÃĄ continuar na versÃŖo Beta atÊ à prÃŗxima actualizaÃ§ÃŖo EstÃĄvel.\n\nPode, manualmente, reverter para a Ãēltima versÃŖo EstÃĄvel. Para isso, faça download e instale a Ãēltima versÃŖo EstÃĄvel manualmente. Note que o retrocesso para versÃĩes anteriores com diferenças significativas poderÃĄ introduzir bugs ou danificar a instalaÃ§ÃŖo.\n\nDeseja abrir o GitHub para fazer download da Ãēltima versÃŖo?"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "SÃŗ Ê possível colar conteÃēdo texto."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Abrir em {URL}"; + diff --git a/L10n/ru.strings b/L10n/ru.strings index c3c0fae1..d302d445 100644 --- a/L10n/ru.strings +++ b/L10n/ru.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "ĐžŅˆĐ¸ĐąĐēа: ĐŊĐĩ ŅƒĐ´Đ°ĐĩŅ‚ŅŅ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ Ņ‚ĐžĐēĐĩĐŊ"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC ŅŅ‚ĐžĐģĐēĐŊ҃ĐģŅŅ ҁ ĐēŅ€Đ¸Ņ‚Đ¸Ņ‡ĐĩҁĐēОК ĐžŅˆĐ¸ĐąĐēОК и ĐŊĐĩ ĐŧĐžĐļĐĩŅ‚ Đ˛ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒŅŅ. EDMC ĐžŅ‚ĐēĐģŅŽŅ‡Đ°ĐĩŅ‚ŅŅ Đ´ĐģŅ ŅĐžĐąŅŅ‚Đ˛ĐĩĐŊĐŊОК ĐˇĐ°Ņ‰Đ¸Ņ‚Ņ‹!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Đ’ĐžŅ‚ Ņ‡Ņ‚Đž ОйĐŊĐ°Ņ€ŅƒĐļиĐģ EDMC:\n\n{ERR}\n\nĐ’Ņ‹ Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ Ой ĐžŅˆĐ¸ĐąĐēĐĩ ĐŊа GitHub?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPI ĐžŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ Đ´ĐģŅ Ņ‚ĐĩŅ…ĐŊĐ¸Ņ‡ĐĩҁĐēĐžĐŗĐž ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "ĐŸŅ€Đ°Đ˛Đēа"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Вид"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "ОĐēĐŊĐž"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "ДоĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊĐž"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đ¸Ņ‚ŅŒ ĐŊаĐģĐ¸Ņ‡Đ¸Đĩ ОйĐŊОвĐģĐĩĐŊиК..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊŅ‹Đš ĐŋŅ€ĐžŅ„Đ°ĐšĐģĐĩŅ€"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ \"ҁҋҀҋĐĩ\" даĐŊĐŊŅ‹Đĩ.."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "КоĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Đ’Ņ‹Ņ€ĐĩĐˇĐ°Ņ‚ŅŒ"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Đ’ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "CAPI Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊа ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ killswitch"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI ĐēĐžŅ€Đ°ĐąĐģĐĩĐš-ĐŊĐžŅĐ¸Ņ‚ĐĩĐģĐĩĐš ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊ ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ killswitch"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: НĐĩŅ‚ даĐŊĐŊҋ҅ Đž Ņ„ĐģĐžŅ‚ĐžĐŊĐžŅŅ†Đĩ"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: НĐĩŅ‚ даĐŊĐŊҋ҅ Đž ĐēĐžŅ€Đ°ĐąĐģĐĩ-ĐŊĐžŅĐ¸Ņ‚ĐĩĐģĐĩ"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: НĐĩĐŋĐžĐģĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Ņ„ĐģĐžŅ‚ĐžĐŊĐžŅŅ†Đĩ"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: НĐĩĐŋĐžĐģĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž ĐēĐžŅ€Đ°ĐąĐģĐĩ-ĐŊĐžŅĐ¸Ņ‚ĐĩĐģĐĩ"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: НĐĩŅ‚ даĐŊĐŊҋ҅ ĐŋиĐģĐžŅ‚Đ°"; @@ -219,6 +231,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: БĐĩС ĐŋОддĐĩŅ€ĐļĐēи Python 3.x"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: НĐĩŅ€Đ°ĐąĐžŅ‚Đ°ŅŽŅ‰Đ¸Đĩ ĐŋĐģĐ°ĐŗĐ¸ĐŊŅ‹"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ОдиĐŊ иĐģи ĐŊĐĩҁĐēĐžĐģҌĐēĐž вĐēĐģŅŽŅ‡ĐĩĐŊĐŊҋ҅ ĐŋĐģĐ°ĐŗĐ¸ĐŊОв. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ĐŋĐžŅĐŧĐžŅ‚Ņ€Đ¸Ņ‚Đĩ ҁĐŋĐ¸ŅĐžĐē ĐŊа вĐēĐģадĐēĐĩ '{PLUGINS}' в Ņ€Đ°ĐˇĐ´ĐĩĐģĐĩ '{FILE}' > '{SETTINGS}'. ĐŸŅ€Đ¸Ņ‡Đ¸ĐŊОК ĐŧĐžĐļĐĩŅ‚ ĐąŅ‹Ņ‚ŅŒ ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐ°Ņ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ° ĐŋаĐŋĐžĐē. ФаКĐģ load.py Đ´ĐžĐģĐļĐĩĐŊ ĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚ŅŒŅŅ в ĐŋаĐŋĐēĐĩ plugins/PLUGIN_NAME/load.py.\n\nĐ’Ņ‹ ĐŧĐžĐļĐĩŅ‚Đĩ ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŋĐģĐ°ĐŗĐ¸ĐŊ, ĐŋĐĩŅ€ĐĩиĐŧĐĩĐŊОвав ĐĩĐŗĐž ĐŋаĐŋĐē҃ Ņ‚Đ°Đē, Ņ‡Ņ‚ĐžĐąŅ‹ в ĐēĐžĐŊ҆Đĩ ĐŊаСваĐŊĐ¸Ņ ŅŅ‚ĐžŅĐģĐž '{DISABLED}'."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "ОдиĐŊ иĐģи ĐŊĐĩҁĐēĐžĐģҌĐēĐž Đ˛Đ°ŅˆĐ¸Ņ… URL-ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ĐžĐ˛ ĐžĐēаСаĐģĐ¸ŅŅŒ ĐŊĐĩĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊŅ‹Đŧи и ĐąŅ‹Đģи ŅĐąŅ€ĐžŅˆĐĩĐŊŅ‹:"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} ĐąŅ‹Đģ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ ĐŊа {OLDPROV} и ĐąŅ‹Đģ ŅĐąŅ€ĐžŅˆĐĩĐŊ ĐŊа {NEWPROV}"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: ŅĐąŅ€ĐžŅ ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ĐžĐ˛ ĐŋĐž ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "ОĐļидаĐŊиĐĩ ĐŋĐžĐģĐŊĐžĐŗĐž Đ˛Ņ…ĐžĐ´Đ° в ŅĐ¸ŅŅ‚ĐĩĐŧ҃"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "ĐšĐ°Ņ‚Đ°ĐģĐžĐŗ ĐļŅƒŅ€ĐŊаĐģа ҃ĐļĐĩ СайĐģĐžĐēĐ¸Ņ€ĐžĐ˛Đ°ĐŊ"; @@ -234,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "По ҃ĐŧĐžĐģŅ‡Đ°ĐŊĐ¸ŅŽ"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēиК"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "ĐĄŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐŊŅ‹Đš"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "БĐĩŅ‚Đ°"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -327,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "ĐžŅˆĐ¸ĐąĐēа: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐēаĐēиĐĩ даĐŊĐŊŅ‹Đĩ ŅŅ‚ĐžĐ¸Ņ‚ ŅĐžŅ…Ņ€Đ°ĐŊŅŅ‚ŅŒ"; @@ -348,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "ĐŸŅƒŅ‚ŅŒ Ņ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ Ņ„Đ°ĐšĐģОв"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "ĐžĐąĐˇĐžŅ€..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ..."; @@ -364,23 +388,11 @@ "CAPI Settings" = "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи CAPI"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ СаĐŋŅ€ĐžŅŅ‹ Đē CAPI Đž Ņ„ĐģĐžŅ‚ĐžĐŊĐžŅŅ†Đĩ"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "ĐĄĐžŅ‡ĐĩŅ‚Đ°ĐŊиĐĩ ĐēĐģĐ°Đ˛Đ¸Ņˆ"; +"Enable Fleet Carrier CAPI Queries" = "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ СаĐŋŅ€ĐžŅŅ‹ Đē CAPI Đž ĐēĐžŅ€Đ°ĐąĐģĐĩ-ĐŊĐžŅĐ¸Ņ‚ĐĩĐģĐĩ"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Đ“ĐžŅ€ŅŅ‡Đ°Ņ ĐēĐģĐ°Đ˛Đ¸ŅˆĐ°"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "ПĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Đ¸Ņ‚Đĩ {APP}, Ņ‡Ņ‚ĐžĐąŅ‹ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ ŅĐžŅ‡ĐĩŅ‚Đ°ĐŊиĐĩ ĐēĐģĐ°Đ˛Đ¸Ņˆ"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} ҂ҀĐĩĐąŅƒĐĩŅ‚ŅŅ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊиĐĩ ĐŊа Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩ ĐŗĐģОйаĐģҌĐŊҋ҅ ŅĐžŅ‡ĐĩŅ‚Đ°ĐŊиК ĐēĐģĐ°Đ˛Đ¸Ņˆ"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "ĐžŅ‚ĐēҀҋ҂ҋ ÂĢĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹Âģ"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "ĐĸĐžĐģҌĐēĐž ĐēĐžĐŗĐ´Đ° ĐžĐēĐŊĐž Elite: Dangerous аĐēŅ‚Đ¸Đ˛ĐŊĐž"; @@ -456,8 +468,11 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "ПаĐŋĐēа ĐŋĐģĐ°ĐŗĐ¸ĐŊОв"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ"; +/* prefs.py: Label on button used to open the Plugin Folder; */ +"Open Plugins Folder" = "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ ĐŋаĐŋĐē҃ ĐŋĐģĐ°ĐŗĐ¸ĐŊОв"; + +/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */ +"Plugin Directory Location" = "МĐĩŅŅ‚ĐžŅ€Đ°ŅĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ Đ´Đ¸Ņ€ĐĩĐēŅ‚ĐžŅ€Đ¸Đ¸ ĐŋĐģĐ°ĐŗĐ¸ĐŊОв"; /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "ĐŸĐžĐ´ŅĐēаСĐēа: ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐēĐžĐŊĐēŅ€ĐĩŅ‚ĐŊŅ‹Đš ĐŋĐģĐ°ĐŗĐ¸ĐŊ ĐŧĐžĐļĐŊĐž {CR} дОйавив '{EXT}' Đē ĐŊаСваĐŊĐ¸ŅŽ ĐŋаĐŋĐēи"; @@ -471,9 +486,15 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž ĐŧĐ¸ĐŗŅ€Đ¸Ņ€ŅƒŅŽŅ‰Đ¸Ņ… ĐŋĐģĐ°ĐŗĐ¸ĐŊĐ°Ņ…"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "НĐĩŅ€Đ°ĐąĐžŅ‚Đ°ŅŽŅ‰Đ¸Đĩ ĐŋĐģĐ°ĐŗĐ¸ĐŊŅ‹"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đĩ ĐŋĐģĐ°ĐŗĐ¸ĐŊŅ‹"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "ĐžŅˆĐ¸ĐąĐēа в ŅĐ¸ŅŅ‚ĐĩĐŧĐŊĐžĐŧ ĐŋŅ€ĐžŅ„Đ°ĐšĐģĐĩŅ€Đĩ"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "БаĐģаĐŊҁ"; @@ -774,3 +795,32 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "ĐšĐžŅ€Đ°ĐąĐģи"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} Đ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "ĐĄŅ‚Đ°ĐąĐ¸ĐģҌĐŊĐ°Ņ"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "ОбĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŧĐ°Ņ€ŅˆŅ€ŅƒŅ‚"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "ОбĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŧĐ°Ņ€ŅˆŅ€ŅƒŅ‚. ИСĐŧĐĩĐŊĐĩĐŊĐž ĐŊа {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "ИСĐŧĐĩĐŊĐĩĐŊ ĐŋŅƒŅ‚ŅŒ ОйĐŊОвĐģĐĩĐŊĐ¸Ņ ҁ \"БĐĩŅ‚Đ°\" ĐŊа \"ĐĄŅ‚Đ°ĐąĐ¸ĐģҌĐŊŅ‹Đš\". Đ’Ņ‹ йОĐģҌ҈Đĩ ĐŊĐĩ ĐąŅƒĐ´ĐĩŅ‚Đĩ ĐŋĐžĐģŅƒŅ‡Đ°Ņ‚ŅŒ ОйĐŊОвĐģĐĩĐŊĐ¸Ņ \"БĐĩŅ‚Đ°\". Đ’Ņ‹ ĐžŅŅ‚Đ°ĐŊĐĩŅ‚ĐĩҁҌ ĐŊа Ņ‚ĐĩĐēŅƒŅ‰ĐĩĐš ĐąĐĩŅ‚Đ°-вĐĩŅ€ŅĐ¸Đ¸ Đ´Đž ҁĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐŗĐž ŅŅ‚Đ°ĐąĐ¸ĐģҌĐŊĐžĐŗĐž Ņ€ĐĩĐģиСа.\n\nĐ’Ņ‹ ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ€ŅƒŅ‡ĐŊŅƒŅŽ вĐĩŅ€ĐŊŅƒŅ‚ŅŒŅŅ Đē ĐŋĐžŅĐģĐĩĐ´ĐŊĐĩĐš вĐĩŅ€ŅĐ¸Đ¸ \"ĐĄŅ‚Đ°ĐąĐ¸ĐģҌĐŊĐ°Ņ\". ДĐģŅ ŅŅ‚ĐžĐŗĐž ĐŊĐĩĐžĐąŅ…ĐžĐ´Đ¸ĐŧĐž ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ и ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŋĐžŅĐģĐĩĐ´ĐŊŅŽŅŽ вĐĩŅ€ŅĐ¸ŅŽ \"ĐĄŅ‚Đ°ĐąĐ¸ĐģҌĐŊĐ°Ņ\" Đ˛Ņ€ŅƒŅ‡ĐŊŅƒŅŽ. ĐžĐąŅ€Đ°Ņ‚Đ¸Ņ‚Đĩ вĐŊиĐŧаĐŊиĐĩ, Ņ‡Ņ‚Đž ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Đĩ ĐŧĐĩĐļĐ´Ņƒ ĐžŅĐŊОвĐŊŅ‹Đŧи вĐĩŅ€ŅĐ¸ŅĐŧи ŅĐž СĐŊĐ°Ņ‡Đ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đŧи иСĐŧĐĩĐŊĐĩĐŊĐ¸ŅĐŧи ĐŧĐžĐŗŅƒŅ‚ вОСĐŊиĐēĐŊŅƒŅ‚ŅŒ ĐžŅˆĐ¸ĐąĐēи иĐģи ĐŋĐžĐģĐŊĐ°Ņ ĐŋĐžĐģĐžĐŧĐēа.\n\nĐĨĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ GitHub, Ņ‡Ņ‚ĐžĐąŅ‹ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐŋĐžŅĐģĐĩĐ´ĐŊŅŽŅŽ вĐĩŅ€ŅĐ¸ŅŽ?"; + +/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */ +"Restart Required" = "ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ ĐŋĐĩŅ€ĐĩСаĐŋ҃ҁĐē"; + +/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */ +"A restart of EDMC is required. EDMC will now restart." = "ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ ĐŋĐĩŅ€ĐĩСаĐŋ҃ҁĐē EDMC. EDMC ҁĐĩĐšŅ‡Đ°Ņ ĐŋĐĩŅ€ĐĩСаĐŋŅƒŅŅ‚Đ¸Ņ‚ŅŅ."; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "НĐĩвОСĐŧĐžĐļĐŊĐž Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŊĐĩŅ‚ĐĩĐēŅŅ‚ĐžĐ˛ĐžĐĩ ŅĐžĐ´ĐĩŅ€ĐļиĐŧĐžĐĩ."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ ҇ĐĩŅ€ĐĩС {URL}"; + +/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */ +"Copy Inara SLEF" = "ĐĄĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ даĐŊĐŊŅ‹Đĩ SLEF Inara"; diff --git a/L10n/sl.strings b/L10n/sl.strings index 1ecb1753..1c038e61 100644 --- a/L10n/sl.strings +++ b/L10n/sl.strings @@ -31,12 +31,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Uredi"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Pogled"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Okno"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Pomoč"; @@ -136,9 +130,6 @@ /* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:953; edsm.py:1043; */ "Error: Can't connect to EDSM" = "Napaka: Povezava z EDSM ni mogoča"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Nastavitve"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Izberite podatke, ki jih Åželite shraniti"; @@ -154,30 +145,15 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Lokacija datoteke"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Spremeni..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Prebrskaj..."; /* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:405; */ "Output" = "Izpis"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "BliÅžnjica"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "BliÅžnjica"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Ponovno poÅženi {APP} za uporabo bliÅžnjic"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} potrebuje dovoljenja za uporabo bliÅžnjic"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Odpri sistemske nastavitve"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Samo ko je Elite:Dangerous aktivna aplikacija "; diff --git a/L10n/sr-Latn-BA.strings b/L10n/sr-Latn-BA.strings index 2950f8b8..46cc4632 100644 --- a/L10n/sr-Latn-BA.strings +++ b/L10n/sr-Latn-BA.strings @@ -1,5 +1,5 @@ /* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ -"Send flight log and CMDR status to EDSM" = "PoÅĄalji log leta i Cmdr status na EDSM"; +"Send flight log and CMDR status to EDSM" = "PoÅĄalji log leta i CMDR status na EDSM"; /* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ "Open Log Folder" = "Otvori folder sa logovima"; @@ -22,7 +22,7 @@ "Error: Invalid Credentials" = "GreÅĄka: Neispravni kredencijali"; /* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:296; */ -"Error: Wrong Cmdr" = "GreÅĄka: PogreÅĄan Cmdr"; +"Error: Wrong Cmdr" = "GreÅĄka: PogreÅĄan CMDR"; /* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:432; companion.py:517; */ "Error" = "GreÅĄka"; @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "GreÅĄka: Nemoguće dobaviti token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC je naiÅĄao na kritičnu greÅĄku i ne moÅže da se oporavi. EDMC će biti ugaÅĄen zbog vlastite zaÅĄtite."; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "EDMC je detektovao sljedeće:\n\n{ERR}\n\nDa li Åželite da izvrÅĄite Bug Report na GitHub-u?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPI nedostupan zbog odrÅžavanja"; @@ -55,7 +61,7 @@ "Error: Check E:D journal file location" = "GreÅĄka: Provjerite lokaciju E:D journal fajla"; /* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */ -"Cmdr" = "Cmdr"; +"Cmdr" = "CMDR"; /* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:915; EDMarketConnector.py:1487; */ "Role" = "Uloga"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Uredi"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Pogled"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Prozor"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Pomoć"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Provjeri nadogradnje..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Otvori System Profiler"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Snimi sirove podatke..."; @@ -123,6 +126,15 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "Kopiraj"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "IzreÅži"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Nalijepi"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Selektuj sve"; + /* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ "CAPI auth disabled by killswitch" = "CAPI autentifikacija onemogućena putem sistemskog prekidača"; @@ -165,11 +177,11 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier onemogućen putem sistemskog prekidača"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Fleetcarrier podaci nisu dobijeni"; +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: Fleet Carrier podaci nisu dobijeni"; -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Fleetcarrier podaci nisu potpuni"; +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Fleet Carrier podaci nisu kompletni"; /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Nema podataka o komandantu"; @@ -213,12 +225,6 @@ /* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jedan ili viÅĄe aktiviranih dodataka (plugins) nemaju podrÅĄku za Python 3.x. Pogledajte listu u '{PLUGINS}' tabu u '{FILE}' > '{SETTINGS}'. Provjerite da li postoji nadograđena verzija ili obavijesite autora da treba da promijeni kod za Python 3.x.\n\nMoÅžete deaktivirati dodatak (plugin) dodavanjem '{DISABLED}' na kraju imena njegovog foldera."; -/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */ -"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevID fajlovi nisu pronađeni. Određene opcije vezane za robu bi mogle da budu nedostupne.\n\nDa li Åželite da otvorite Wiki stranicu sa uputstvom za podeÅĄavanje podmodula?"; - -/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */ -"FDevIDs: Missing Commodity Files" = "FDevIDs: Nedostaju Commodity fajlovi"; - /* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ "Plugins" = "Dodaci (plugins)"; @@ -231,6 +237,18 @@ /* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ "One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jedan ili viÅĄe aktiviranih dodataka (plugins) nemaju podrÅĄku za Python 3.x. Pogledajte listu u '{PLUGINS}' tabu u '{FILE}' > '{SETTINGS}'. PogreÅĄna struktura foldera moÅže da uzrokuje problem. Fajl load.py treba da bude smjeÅĄten u plugins/PLUGIN_NAME/load.py.\n\nMoÅžete deaktivirati dodatak (plugin) dodavanjem '{DISABLED}' na kraju imena njegovog foldera."; +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Jedan ili viÅĄe URL Providera su pogreÅĄni i zbog toga su resetovani:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} je bio postavljen na {OLDPROV}, a sad je resetovan na {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Standardni Provideri su resetovani"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Čekam da se CMDR potpuno uloguje"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "Journal direktorijum je već zaključan"; @@ -246,13 +264,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Standardna"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Automatski"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normalni"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -325,7 +343,7 @@ "Error: Can't connect to EDSM" = "GreÅĄka: Nemoguće povezivanje sa EDSM"; /* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:257; */ -"Send flight log and Cmdr status to Inara" = "PoÅĄalji log leta i Cmdr status na Inara"; +"Send flight log and Cmdr status to Inara" = "PoÅĄalji log leta i CMDR status na Inara"; /* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:269; */ "Inara credentials" = "Kredencijali za Inara"; @@ -339,9 +357,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "GreÅĄka: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "PodeÅĄavanja"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Izaberite koji podaci se snimaju"; @@ -360,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Lokacija fajlova"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Promijeni..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "PotraÅži..."; @@ -376,23 +388,11 @@ "CAPI Settings" = "CAPI PodeÅĄavanja"; /* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Omogući Fleetcarrier CAPI Upite"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Prečica"; +"Enable Fleet Carrier CAPI Queries" = "Omogući Fleet Carrier CAPI upite"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Prečica"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Restartujte {APP} da bi ste koristili prečice"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} traÅži dozvolu da koristi prečice"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Otvori sistemska podeÅĄavanja"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Samo kada je Elite: Dangerous aktivna aplikacija"; @@ -468,9 +468,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Folder za dodatke (plugins)"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Otvori"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Savjet: MoÅžete da deaktivirate dodatak (plugin){CR}dodavanjem '{EXT}' na ime njegovog foldera"; @@ -489,6 +486,9 @@ /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Deaktivirani dodaci (plugins)"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "GreÅĄa u System Profileru"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "Balans"; @@ -789,3 +789,24 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Brodovi"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} je dostupna"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "Stabilna"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Kanal osvjeÅžavanja"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Kanal za osvjeÅžavanje je promijenjen u {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Kanal za osvjeÅžavanje je promijenjen iz Beta u Stabilni. ViÅĄe nećete primati Beta osvjeÅžavanja. Ostaćete na trenutnoj Beta verziji do sljedeće Stabilne verzije.\n\nMoÅžete se ručno vratiti na posljednju Stabilnu verziju. Da biste to učinili morate da skinete i ručno instaliÅĄete posljednju Stabilnu verziju. Ova procedura moÅže da uvede nove bugove ili da potpuno onesposobi program ako prelazite između verzija sa mnogo značajnih promjena.\n\nDa li Åželite da otvorite GitHub stranicu za download posljednje verzije?"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Nije moguće da se zalijepi netekstualni sadrÅžaj."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Otvori u {URL}"; + diff --git a/L10n/sr-Latn.strings b/L10n/sr-Latn.strings index 5f2c01c5..6fb8b7db 100644 --- a/L10n/sr-Latn.strings +++ b/L10n/sr-Latn.strings @@ -36,6 +36,12 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "GreÅĄka: nemoguće dobaviti token"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC je naiÅĄao na kritičnu greÅĄku i ne moÅže da se oporavi. EDMC će biti ugaÅĄen radi svoje zaÅĄtite."; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Evo ÅĄta je EDMC detektovao:\n\n{ERR}\n\nDa li Åželite da napravite Bug Report na GitHub-u?"; + /* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ "Frontier CAPI down for maintenance" = "Frontier CAPI isključen radi odrÅžavanja"; @@ -78,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Izmeni"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Pogled"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Prozor"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Pomoć"; @@ -93,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "Proveri da li postoje izmene..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Otvori System Profiler"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "Snimi sirove podatke..."; @@ -165,12 +168,6 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier deaktiviran preko sistemskog prekidača"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPI: Podaci o fleetcarrier-u nisu dobijeni"; - -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPI: Podaci o fleetcarrier-u su nepotpuni"; - /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: Nema podataka o komandiru"; @@ -219,6 +216,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: Dodaci (plugins) bez Python 3.x podrÅĄke"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Pokvareni Pluginovi"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jedan ili viÅĄe vaÅĄih aktivnih pluginova se nije učitao. Molimo pogledajte listu na '{PLUGINS}' tabu u okviru '{FILE}' > '{SETTINGS}'. Obo vi moglo da se desi zbog pogreÅĄne strukture foldera. load.py fajl bi trebao da se nalazi unutar plugins/PLUGIN_NAME/load.py.\n\nPojedinačni plugin moÅžete deaktivirati promenom imena njegovog foldera dodavanjem '{DISABLED}' na kraju."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Jedan ili viÅĄe vaÅĄih URL Provajdera su pogreÅĄni, pa su resetovani:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} je bio postavljen na {OLDPROV} i sada je resetovan na {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: Podrazumevani Provajderi resetovani"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Čekam da se CMDR potpuno uloguje"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "ÅŊurnal direktorijum je već zaključan"; @@ -234,13 +249,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "Podrazumevano"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "Auto"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "Normal"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "Beta"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -327,9 +342,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "GreÅĄka: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "PodeÅĄavanja"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Izaberite koji se podaci snimaju"; @@ -348,9 +360,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Lokacija fajlova"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Izmeni..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "PotraÅži..."; @@ -363,24 +372,9 @@ /* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */ "CAPI Settings" = "CAPI PodeÅĄavanja"; -/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "Aktiviraj Fleetcarrier CAPIU Upite"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Skraćenica"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Skraćenica"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Restartujte {APP} da bi ste koristili skraćenice"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} traÅži dozvolu da koristi skraćenice"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Otvori sistemska podeÅĄavanja"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Samo kada je Elite: Dangerous aktivna aplikacija"; @@ -456,9 +450,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Folder za dodatke (plugins)"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Otvori"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: MoÅžete deaktivirati dodatak (plugin) dodavanjem{CR}'{EXT}' na ime njegovog foldera"; @@ -471,6 +462,9 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "Informacije o migraciji dodataka (plugins)"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Pokvareni pluginovi"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "Deaktivirani dodaci (plugins)"; @@ -774,3 +768,12 @@ /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "Brodovi"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} je dostupna"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Nemoguće je nalepiti netekstualni sadrÅžaj."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Otvori na {URL}"; + diff --git a/L10n/sv-SE.strings b/L10n/sv-SE.strings index 8f944159..4bcce399 100644 --- a/L10n/sv-SE.strings +++ b/L10n/sv-SE.strings @@ -37,12 +37,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Ändra"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "View"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "FÃļnster"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Hjälp"; @@ -175,9 +169,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "Fel: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "Inställningar"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "Välj vilket data som skall lagras"; @@ -196,9 +187,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "FilsÃļkväg"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "Ändra..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "Bläddra..."; @@ -208,21 +196,9 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "E:D journal-fil sÃļkväg"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "Genväg, tangentbord"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Snabbkommando"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "Starta om {APP} fÃļr att använda genvägar"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} behÃļver rättigheter fÃļr att använda genvägar"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Öppna systeminställningar"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "Bara när: Elite Dangerous är det aktiva programmet"; @@ -265,9 +241,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "Mapp fÃļr plugins"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Öppna"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tips: du kan avaktivera en plugin genom{CR}att lägga till '{EXT}' pÃĨ slutet av mappens namn"; diff --git a/L10n/tr.strings b/L10n/tr.strings new file mode 100644 index 00000000..80a4375c --- /dev/null +++ b/L10n/tr.strings @@ -0,0 +1,812 @@ +/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ +"Send flight log and CMDR status to EDSM" = "Uçuş gÃŧnlÃŧğÃŧnÃŧ ve CMDR durumunu EDSM'e gÃļnder"; + +/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ +"Open Log Folder" = "GÃŧnlÃŧk KlasÃļrÃŧnÃŧ Aç"; + +/* inara.py:Text Inara Show API key; In files: inara.py:305; */ +"Show API Key" = "API AnahtarÄąnÄą GÃļster"; +/* Language name */ +"!Language" = "TÃŧrkçe"; + +/* companion.py: Frontier CAPI didn't respond; In files: companion.py:226; */ +"Error: Frontier CAPI didn't respond" = "Hata: Frontier CAPI yanÄąt vermedi"; + +/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:245; */ +"Error: Frontier server is lagging" = "Hata: Frontier sunucusu gecikmesi"; + +/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:261; */ +"Docked but unknown station: EDO Settlement?" = "Kenetlendi ancak bilinmeyen istasyon: EDO yerleşimi?"; + +/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:271; */ +"Error: Invalid Credentials" = "Hata: Geçersiz Kimlik"; + +/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:296; */ +"Error: Wrong Cmdr" = "Hata: YanlÄąÅŸ CMDR"; + +/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:432; companion.py:517; */ +"Error" = "Hata"; + +/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:475; companion.py:480; */ +"Error: Couldn't check token customer_id" = "Hata: token kontrol edilemedi customer_id"; + +/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:486; */ +"Error: customer_id doesn't match!" = "Hata: customer_id uyuşmuyor!"; + +/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ +"Error: unable to get token" = "Hata: token erişilemedi"; + +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC kritik bir hatayla karÅŸÄąlaştÄą ve kurtarÄąlamÄąyor. EDMC kendi korumasÄą için kapanÄąyor!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "EDMC tespit edilen hata:\n\n{ERR}\n\nGitHub'da bir Hata Raporu gÃļndermek ister misiniz?"; + +/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ +"Frontier CAPI down for maintenance" = "Frontier CAPI bakÄąm için kapalÄą durumda"; + +/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:856; */ +"Frontier CAPI query failure" = "Frontier CAPI sorgulama hatasÄą"; + +/* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */ +"Update" = "GÃŧncelle"; + +/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:710; prefs.py:875; */ +"Always on top" = "Her zaman Ãŧstte"; + +/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:837; */ +"Unknown" = "Bilinmeyen"; + +/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:906; */ +"Error: Check E:D journal file location" = "Hata: E:D gÃŧnlÃŧk dosyasÄą konumunu kontrol edin"; + +/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */ +"Cmdr" = "Cmdr"; + +/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:915; EDMarketConnector.py:1487; */ +"Role" = "Rol"; + +/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:915; EDMarketConnector.py:1497; EDMarketConnector.py:1520; stats.py:405; */ +"Ship" = "Gemi"; + +/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:916; */ +"Suit" = "SÃŧit"; + +/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:917; prefs.py:614; stats.py:407; */ +"System" = "Sistem"; + +/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:918; prefs.py:632; prefs.py:770; stats.py:408; */ +"Station" = "İstasyon"; + +/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:921; EDMarketConnector.py:939; EDMarketConnector.py:942; EDMarketConnector.py:2264; */ +"File" = "Dosya"; + +/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ +"Edit" = "DÃŧzenle"; + +/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ +"Help" = "YardÄąm"; + +/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:928; EDMarketConnector.py:959; EDMarketConnector.py:1804; */ +"About {APP}" = "HakkÄąnda {APP}"; + +/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ +"Check for Updates..." = "GÃŧncellemeleri Denetle..."; + +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Sistem Profilcisi'ni aÃ§Äąn"; + +/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ +"Save Raw Data..." = "Ham Verileri Kaydet..."; + +/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:932; EDMarketConnector.py:947; stats.py:402; */ +"Status" = "Durum"; + +/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:933; EDMarketConnector.py:953; */ +"Documentation" = "DokÃŧmantasyon"; + +/* EDMarketConnector.py: Help > Troubleshooting; In files: EDMarketConnector.py:934; EDMarketConnector.py:954; */ +"Troubleshooting" = "Sorun giderme"; + +/* EDMarketConnector.py: Help > Report A Bug; In files: EDMarketConnector.py:935; EDMarketConnector.py:955; */ +"Report A Bug" = "Hata Bildir"; + +/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:936; EDMarketConnector.py:956; */ +"Privacy Policy" = "Gizlilik PolitikasÄą"; + +/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:937; EDMarketConnector.py:957; EDMarketConnector.py:1838; */ +"Release Notes" = "SÃŧrÃŧm notlarÄą"; + +/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:949; EDMarketConnector.py:2265; prefs.py:241; */ +"Settings" = "Ayarlar"; + +/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:950; */ +"Exit" = "Ã‡ÄąkÄąÅŸ"; + +/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ +"Copy" = "Kopyala"; + +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Kes"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "YapÄąÅŸtÄąr"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "TÃŧmÃŧnÃŧ Seç"; + +/* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ +"CAPI auth disabled by killswitch" = "CAPI kimlik doğrulamasÄą killswitch tarafÄąndan devre dÄąÅŸÄą bÄąrakÄąldÄą"; + +/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:978; */ +"Logging in..." = "Giriş yapÄąlÄąyor..."; + +/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:994; EDMarketConnector.py:1657; */ +"Authentication successful" = "Kimlik doğrulama başarÄąlÄą"; + +/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:1025; */ +"You're not docked at a station!" = "Bir istasyona kenetli değilsiniz!"; + +/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:1033; */ +"Station doesn't have anything!" = "İstasyon içeriği mevcut değil."; + +/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:1038; */ +"Station doesn't have a market!" = "İstasyon'da market bulunmuyor!"; + +/* EDMarketConnector.py: CAPI queries aborted because Cmdr name is unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because Cmdr name is unknown; In files: EDMarketConnector.py:1077; EDMarketConnector.py:1164; */ +"CAPI query aborted: Cmdr name unknown" = "CAPI sorgusu iptal edildi: Cmdr adÄą bilinmiyor"; + +/* EDMarketConnector.py: CAPI queries aborted because game mode unknown; In files: EDMarketConnector.py:1083; */ +"CAPI query aborted: Game mode unknown" = "CAPI sorgusu iptal edildi: Oyun modu bilinmiyor"; + +/* EDMarketConnector.py: CAPI queries aborted because GameVersion unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because GameVersion unknown; In files: EDMarketConnector.py:1089; EDMarketConnector.py:1170; */ +"CAPI query aborted: GameVersion unknown" = "CAPI sorgusu iptal edildi: GameVersion bilinmiyor"; + +/* EDMarketConnector.py: CAPI queries aborted because current star system name unknown; In files: EDMarketConnector.py:1095; */ +"CAPI query aborted: Current system unknown" = "CAPI sorgusu iptal edildi: Mevcut sistem bilinmiyor"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in multi-crew on other Cmdr's ship; In files: EDMarketConnector.py:1101; */ +"CAPI query aborted: In other-ship multi-crew" = "CAPI sorgusu iptal edildi: Başka gemi çoklu-mÃŧrettebatÄą"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:1107; */ +"CAPI query aborted: CQC (Arena) detected" = "CAPI sorgusu iptal edildi: CQC (Arena) tespit edildi"; + +/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; In files: EDMarketConnector.py:1128; EDMarketConnector.py:1179; */ +"Fetching data..." = "Veri işleniyor..."; + +/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ +"CAPI fleetcarrier disabled by killswitch" = "CAPI filo taÅŸÄąyÄącÄąsÄą killswitch tarafÄąndan devre dÄąÅŸÄą bÄąrakÄąldÄą"; + +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: Filo TaÅŸÄąyÄącÄąsÄą verisi bulunamadÄą"; + +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: Filo TaÅŸÄąyÄącÄąsÄą verileri eksik"; + +/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ +"CAPI: No commander data returned" = "CAPI: Cmdr verisi dÃļndÃŧrÃŧlmedi"; + +/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:1246; stats.py:333; */ +"Who are you?!" = "Sen kimsin?!"; + +/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:1252; stats.py:341; */ +"Where are you?!" = "Neredesin?!"; + +/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1259; stats.py:349; */ +"What are you flying?!" = "Ne uçuruyorsun?!"; + +/* EDMarketConnector.py: Frontier CAPI server error when fetching data; In files: EDMarketConnector.py:1384; */ +"Frontier CAPI server error" = "Frontier CAPI sunucu hatasÄą"; + +/* EDMarketConnector.py: Frontier CAPI Access Token expired, trying to get a new one; In files: EDMarketConnector.py:1390; */ +"CAPI: Refreshing access token..." = "CAPI: Erişim token yenileniyor..."; + +/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1434; */ +"Last updated at %H:%M:%S" = "Son gÃŧncelleme %H:%M:%S"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1462; */ +"Fighter" = "AvcÄą"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1463; */ +"Gunner" = "NişancÄą"; + +/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1464; */ +"Helm" = "DÃŧmen"; + +/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1742; */ +"cooldown {SS}s" = "bekleme sÃŧresi {SS}s"; + +/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1864; prefs.py:292; */ +"OK" = "Tamam"; + +/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1936; */ +"Shutting down..." = "KapanÄąyor..."; + +/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */ +"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Etkin eklentilerinizden bir veya daha fazlasÄą henÃŧz Python 3.x desteğine sahip değil. '{PLUGINS}' sekmesindeki '{FILE}' > '{SETTINGS}' bÃļlÃŧmÃŧnde bulunan listeyi bulun. GÃŧncellenmiş bir sÃŧrÃŧmÃŧn mevcut olup olmadığınÄą kontrol edin, aksi takdirde geliştiriciyi Python 3.x kodunu gÃŧncellemesi gerektiği konusunda uyarmalÄąsÄąnÄąz.\n\nBir eklentiyi, klasÃļrÃŧnÃŧ adÄąnÄąn sonunda '{DISABLED}' olacak şekilde yeniden adlandÄąrarak devre dÄąÅŸÄą bÄąrakabilirsiniz."; + +/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */ +"Plugins" = "Eklentiler"; + +/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ +"EDMC: Plugins Without Python 3.x Support" = "EDMC: Python 3.x Desteği Olmayan Eklentiler"; + +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: Bozuk Eklentiler"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Etkin eklentilerinizden bir veya daha fazlasÄą yÃŧklenemedi. '{PLUGINS}' sekmesindeki '{FILE}' > '{SETTINGS}' bÃļlÃŧmÃŧnde bulunan listeyi bulun. Sorunun sebebi yanlÄąÅŸ klasÃļr yapÄąsÄą olabilir. Load.py dosyasÄą, plugins/PLUGIN_NAME/load.py altÄąnda bulunmalÄądÄąr.\n\nBir eklentiyi, klasÃļrÃŧnÃŧ adÄąnÄąn sonunda '{DISABLED}' olacak şekilde yeniden adlandÄąrarak devre dÄąÅŸÄą bÄąrakabilirsiniz."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "URL SağlayÄącÄąlarÄąnÄązdan bir veya daha fazlasÄą geçersizdi ve sÄąfÄąrlandÄą:"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} , {OLDPROV}'a ayarlanmÄąÅŸtÄą ancak {NEWPROV}'a sÄąfÄąrlandÄą"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: VarsayÄąlan SağlayÄącÄąlarÄąn SÄąfÄąrlanmasÄą"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "Tam CMDR Girişi Bekleniyor"; + +/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ +"Journal directory already locked" = "GÃŧnlÃŧk dizini zaten kilitli"; + +/* journal_lock.py: Text for when newly selected Journal directory is already locked; In files: journal_lock.py:225:226; */ +"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "Yeni GÃŧnlÃŧk Dizini konumu zaten kilitli.{CR}ÇÃļzÃŧm için Tekrar deneyebilir veya Yoksay'Äą seçebilirsiniz."; + +/* journal_lock.py: Generic 'Retry' button label; In files: journal_lock.py:230; */ +"Retry" = "Tekrar Dene"; + +/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:234; */ +"Ignore" = "Yoksay"; + +/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ +"Default" = "VarsayÄąlan"; + +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ +"Auto" = "Otomatik"; + +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ +"Normal" = "Normal"; + +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ +"Beta" = "Beta"; + +/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ +"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Coriolis.io gemi ekipman tasarÄąmlarÄą ile kullanÄąlacak URL'yi ayarlayÄąn. Link'in '/import?data=' ile bitmesi GEREKTİĞİNİ unutmayÄąn."; + +/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: coriolis.py:97; */ +"Normal URL" = "Normal URL"; + +/* coriolis.py: Generic 'Reset' button label; In files: coriolis.py:100; coriolis.py:109; */ +"Reset" = "SÄąfÄąrla"; + +/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: coriolis.py:106; */ +"Beta URL" = "Beta URL"; + +/* coriolis.py: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL; In files: coriolis.py:116; */ +"Override Beta/Normal Selection" = "Beta/Normal Seçimi Geçersiz KÄąl"; + +/* coriolis.py: Settings>Coriolis - invalid override mode found; In files: coriolis.py:156; */ +"Invalid Coriolis override mode!" = "Geçersiz Coriolis geçersiz kÄąlma modu!"; + +/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:458; eddn.py:2413; eddn.py:2451; eddn.py:2519; */ +"Error: Can't connect to EDDN" = "Hata: EDDN'e bağlanÄąlamÄąyor"; + +/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:576; */ +"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN HatasÄą: EDMC sÃŧrÃŧmÃŧ EDDN için çok eski. LÃŧtfen gÃŧncelle."; + +/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:582; */ +"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN HatasÄą: Doğrulama BaşarÄąsÄąz Oldu (EDMC sÃŧrÃŧmÃŧ eski olabilir?). GÃŧnlÃŧğe bakÄąn"; + +/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:587; */ +"EDDN Error: Returned {STATUS} status code" = "EDDN HatasÄą: {STATUS} durum kodu ile dÃļndÃŧ."; + +/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:2041; */ +"Send station data to the Elite Dangerous Data Network" = "İstasyon verilerini Elite Dangerous Data Network'e gÃļnderin"; + +/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:2052; */ +"Send system and scan data to the Elite Dangerous Data Network" = "Sistem ve tarama verilerini Elite Dangerous Data Network'e gÃļnderin"; + +/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:2063; */ +"Delay sending until docked" = "Kenetlenmeye kadar gÃļnderimi ertele"; + +/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:2178; */ +"EDDN journal handler disabled. See Log." = "EDDN gÃŧnlÃŧk dÃŧzenleyici devre dÄąÅŸÄą. KayÄąt geçmişini kontrol edin."; + +/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:2507; */ +"Sending data to EDDN..." = "Veriler EDDN'e gÃļnderiliyor..."; + +/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:319; */ +"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map kimlik bilgileri"; + +/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:341; */ +"Commander Name" = "Cmdr ismi"; + +/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:350; inara.py:278; */ +"API Key" = "API AnahtarÄą"; + +/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:394; prefs.py:527; prefs.py:1157; prefs.py:1190; stats.py:154; stats.py:173; stats.py:192; stats.py:209; */ +"None" = "Hiçbiri"; + +/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:516; */ +"EDSM Handler disabled. See Log." = "EDSM dÃŧzenleyici devre dÄąÅŸÄą. KayÄąt geçmişini kontrol edin."; + +/* edsm.py: EDSM - Only Live data; In files: edsm.py:632; */ +"EDSM only accepts Live galaxy data" = "EDSM sadece canlÄą galaxy verilerini kabul eder"; + +/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:916; edsm.py:1048; */ +"Error: EDSM {MSG}" = "Hata: EDSM {MSG}"; + +/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:953; edsm.py:1043; */ +"Error: Can't connect to EDSM" = "Hata: EDSM'e bağlanÄąlamÄąyor"; + +/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:257; */ +"Send flight log and Cmdr status to Inara" = "Uçuş gÃŧnlÃŧğÃŧnÃŧ ve Cmdr durumunu Inara'ya gÃļnder"; + +/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:269; */ +"Inara credentials" = "Inara kimlik bilgileri"; + +/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; inara.py: Inara - Only Live data; In files: inara.py:384; inara.py:386; */ +"Inara only accepts Live galaxy data" = "Inara sadece canlÄą galaxy verilerini kabul eder"; + +/* inara.py: INARA support disabled via killswitch; In files: inara.py:395; */ +"Inara disabled. See Log." = "Inara devre dÄąÅŸÄą. KayÄąt geçmişini kontrol edin."; + +/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ +"Error: Inara {MSG}" = "Hata: Inara {MSG}"; + +/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ +"Please choose what data to save" = "LÃŧtfen hangi verilerin kaydedileceğini seçin"; + +/* prefs.py: Settings > Output option; In files: prefs.py:341; */ +"Market data in CSV format file" = "CSV formatÄąnda Market verileri"; + +/* prefs.py: Settings > Output option; In files: prefs.py:350; */ +"Market data in Trade Dangerous format file" = "Trade Dangerous formatÄąnda Market verileri"; + +/* prefs.py: Settings > Output option; In files: prefs.py:360; */ +"Ship loadout" = "Gemi EkipmanlarÄą"; + +/* prefs.py: Settings > Output option; In files: prefs.py:370; */ +"Automatically update on docking" = "Kenetlenme sÄąrasÄąnda otomatik gÃŧncelle"; + +/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ +"File location" = "Dosya konumu"; + +/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ +"Browse..." = "Gezin..."; + +/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:405; */ +"Output" = "KayÄąt"; + +/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ +"E:D journal file location" = "E:D gÃŧnlÃŧk dosya konumu"; + +/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */ +"CAPI Settings" = "CAPI ayarlarÄą"; + +/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ +"Enable Fleet Carrier CAPI Queries" = "Filo TaÅŸÄąyÄącÄą CAPI SorgularÄąnÄą Etkinleştir"; + +/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ +"Hotkey" = "KÄąsayoltuşu"; + +/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ +"Only when Elite: Dangerous is the active app" = "Sadece Elite: Dangerous aktif uygulama olduğunda"; + +/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:549; */ +"Play sound" = "Ses çal"; + +/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:564; */ +"Disable Automatic Application Updates Check when in-game" = "Otomatik Uygulama GÃŧncellemelerini Oyun sÄąrasÄąnda Devre DÄąÅŸÄą BÄąrak"; + +/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:577; */ +"Preferred websites" = "Tercih edilen web siteleri"; + +/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:588; */ +"Shipyard" = "Tersane"; + +/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:600; */ +"Use alternate URL method" = "Alternatif URL yÃļntemi kullan"; + +/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:653; */ +"Log Level" = "GÃŧnlÃŧk Seviyesi"; + +/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:681; */ +"Configuration" = "YapÄąlandÄąrma"; + +/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:690; */ +"Main UI privacy options" = "Ana ArayÃŧz gizlilik seçenekleri"; + +/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:695; */ +"Hide private group name in UI" = "Özel grup ismini arayÃŧzde gizle"; + +/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:699; */ +"Hide multi-crew captain name" = "Çoklu-mÃŧrettebat ismini gizle"; + +/* prefs.py: Preferences privacy tab title; In files: prefs.py:703; */ +"Privacy" = "Gizlilik"; + +/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:716; */ +"Normal text" = "Normal metin"; + +/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:718; */ +"Highlighted text" = "Vurgulanan metin"; + +/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:727; */ +"Language" = "Dil"; + +/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:737; */ +"Theme" = "Tema"; + +/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:749; */ +"Dark" = "KaranlÄąk"; + +/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:756; */ +"Transparent" = "ŞeffaflÄąk"; + +/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:802; */ +"UI Scale Percentage" = "ArayÃŧz Ãļlçek yÃŧzdesi"; + +/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:823; */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 VarsayÄąlan değerdir. Değişikliklerin geçerli olmasÄą için {CR}Yeniden Başlatma Gereklidir{CR}!"; + +/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:833; */ +"Main window transparency" = "Ana pencere şeffaflığı"; + +/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:853:856; */ +"100 means fully opaque.{CR}Window is updated in real time" = "100 tamamen opak anlamÄąna gelir.{CR}Pencere gerçek zamanlÄą olarak gÃŧncellenir"; + +/* prefs.py: Appearance option for Windows "minimize to system tray"; In files: prefs.py:885; */ +"Minimize to system tray" = "Simge durumuna kÃŧçÃŧlt"; + +/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:893; */ +"Appearance" = "GÃļrÃŧnÃŧm"; + +/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ +"Plugins folder" = "Eklentiler klasÃļrÃŧ"; + +/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ +"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "İpucu: Bir eklentiyi, klasÃļr adÄąna{CR}'{EXT}' ekleyerek devre dÄąÅŸÄą bÄąrakabilirsiniz"; + +/* prefs.py: Label on list of enabled plugins; In files: prefs.py:934; */ +"Enabled Plugins" = "Etkin Eklentiler"; + +/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:954; */ +"Plugins Without Python 3.x Support" = "Python 3.x Desteği Olmayan Eklentiler"; + +/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ +"Information on migrating plugins" = "Eklentilerin taÅŸÄąnmasÄąyla ilgili bilgiler"; + +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "Bozuk Eklentiler"; + +/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ +"Disabled Plugins" = "Devre DÄąÅŸÄą Eklentiler"; + +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "Sistem Profilcisinde Hata oluştu"; + +/* stats.py: Cmdr stats; In files: stats.py:58; */ +"Balance" = "Bakiye"; + +/* stats.py: Cmdr stats; In files: stats.py:59; */ +"Loan" = "Borç"; + +/* stats.py: Top rank; In files: stats.py:63; */ +"Elite" = "Elite"; + +/* stats.py: Top rank +1; In files: stats.py:64; */ +"Elite I" = "Elite I"; + +/* stats.py: Top rank +2; In files: stats.py:65; */ +"Elite II" = "Elite II"; + +/* stats.py: Top rank +3; In files: stats.py:66; */ +"Elite III" = "Elite III"; + +/* stats.py: Top rank +4; In files: stats.py:67; */ +"Elite IV" = "Elite IV"; + +/* stats.py: Top rank +5; In files: stats.py:68; */ +"Elite V" = "Elite V"; + +/* stats.py: Ranking; In files: stats.py:74; */ +"Combat" = "Combat"; + +/* stats.py: Ranking; In files: stats.py:75; */ +"Trade" = "Trade"; + +/* stats.py: Ranking; In files: stats.py:76; */ +"Explorer" = "Explorer"; + +/* stats.py: Ranking; In files: stats.py:77; */ +"Mercenary" = "Mercenary"; + +/* stats.py: Ranking; In files: stats.py:78; */ +"Exobiologist" = "Exobiologist"; + +/* stats.py: Ranking; In files: stats.py:79; */ +"CQC" = "CQC"; + +/* stats.py: Ranking; In files: stats.py:80; */ +"Federation" = "Federation"; + +/* stats.py: Ranking; In files: stats.py:81; */ +"Empire" = "Empire"; + +/* stats.py: Ranking; In files: stats.py:82; */ +"Powerplay" = "Powerplay"; + +/* stats.py: Combat rank; In files: stats.py:91; */ +"Harmless" = "Harmless"; + +/* stats.py: Combat rank; In files: stats.py:92; */ +"Mostly Harmless" = "Mostly Harmless"; + +/* stats.py: Combat rank; In files: stats.py:93; */ +"Novice" = "Novice"; + +/* stats.py: Combat rank; In files: stats.py:94; */ +"Competent" = "Competent"; + +/* stats.py: Combat rank; In files: stats.py:95; */ +"Expert" = "Expert"; + +/* stats.py: Combat rank; stats.py: Empire rank; In files: stats.py:96; stats.py:176; */ +"Master" = "Master"; + +/* stats.py: Combat rank; In files: stats.py:97; */ +"Dangerous" = "Dangerous"; + +/* stats.py: Combat rank; In files: stats.py:98; */ +"Deadly" = "Deadly"; + +/* stats.py: Trade rank; In files: stats.py:101; */ +"Penniless" = "Penniless"; + +/* stats.py: Trade rank; In files: stats.py:102; */ +"Mostly Penniless" = "Mostly Penniless"; + +/* stats.py: Trade rank; In files: stats.py:103; */ +"Peddler" = "Peddler"; + +/* stats.py: Trade rank; In files: stats.py:104; */ +"Dealer" = "Dealer"; + +/* stats.py: Trade rank; In files: stats.py:105; */ +"Merchant" = "Merchant"; + +/* stats.py: Trade rank; In files: stats.py:106; */ +"Broker" = "Broker"; + +/* stats.py: Trade rank; In files: stats.py:107; */ +"Entrepreneur" = "Entrepreneur"; + +/* stats.py: Trade rank; In files: stats.py:108; */ +"Tycoon" = "Tycoon"; + +/* stats.py: Explorer rank; In files: stats.py:111; */ +"Aimless" = "Aimless"; + +/* stats.py: Explorer rank; In files: stats.py:112; */ +"Mostly Aimless" = "Mostly Aimless"; + +/* stats.py: Explorer rank; In files: stats.py:113; */ +"Scout" = "Scout"; + +/* stats.py: Explorer rank; In files: stats.py:114; */ +"Surveyor" = "Surveyor"; + +/* stats.py: Explorer rank; In files: stats.py:115; */ +"Trailblazer" = "Trailblazer"; + +/* stats.py: Explorer rank; In files: stats.py:116; */ +"Pathfinder" = "Pathfinder"; + +/* stats.py: Explorer rank; In files: stats.py:117; */ +"Ranger" = "Ranger"; + +/* stats.py: Explorer rank; In files: stats.py:118; */ +"Pioneer" = "Pioneer"; + +/* stats.py: Mercenary rank; In files: stats.py:122; */ +"Defenceless" = "Defenceless"; + +/* stats.py: Mercenary rank; In files: stats.py:123; */ +"Mostly Defenceless" = "Mostly Defenceless"; + +/* stats.py: Mercenary rank; In files: stats.py:124; */ +"Rookie" = "Rookie"; + +/* stats.py: Mercenary rank; In files: stats.py:125; */ +"Soldier" = "Soldier"; + +/* stats.py: Mercenary rank; In files: stats.py:126; stats.py:128; */ +"Gunslinger" = "Gunslinger"; + +/* stats.py: Mercenary rank; In files: stats.py:127; */ +"Warrior" = "Warrior"; + +/* stats.py: Mercenary rank; In files: stats.py:129; */ +"Deadeye" = "Deadeye"; + +/* stats.py: Exobiologist rank; In files: stats.py:132; */ +"Directionless" = "Directionless"; + +/* stats.py: Exobiologist rank; In files: stats.py:133; */ +"Mostly Directionless" = "Mostly Directionless"; + +/* stats.py: Exobiologist rank; In files: stats.py:134; */ +"Compiler" = "Compiler"; + +/* stats.py: Exobiologist rank; In files: stats.py:135; */ +"Collector" = "Collector"; + +/* stats.py: Exobiologist rank; In files: stats.py:136; */ +"Cataloguer" = "Cataloguer"; + +/* stats.py: Exobiologist rank; In files: stats.py:137; */ +"Taxonomist" = "Taxonomist"; + +/* stats.py: Exobiologist rank; In files: stats.py:138; */ +"Ecologist" = "Ecologist"; + +/* stats.py: Exobiologist rank; In files: stats.py:139; */ +"Geneticist" = "Geneticist"; + +/* stats.py: CQC rank; In files: stats.py:142; */ +"Helpless" = "Helpless"; + +/* stats.py: CQC rank; In files: stats.py:143; */ +"Mostly Helpless" = "Mostly Helpless"; + +/* stats.py: CQC rank; In files: stats.py:144; */ +"Amateur" = "Amateur"; + +/* stats.py: CQC rank; In files: stats.py:145; */ +"Semi Professional" = "Semi Professional"; + +/* stats.py: CQC rank; In files: stats.py:146; */ +"Professional" = "Professional"; + +/* stats.py: CQC rank; In files: stats.py:147; */ +"Champion" = "Champion"; + +/* stats.py: CQC rank; In files: stats.py:148; */ +"Hero" = "Hero"; + +/* stats.py: CQC rank; In files: stats.py:149; */ +"Gladiator" = "Gladiator"; + +/* stats.py: Federation rank; In files: stats.py:155; */ +"Recruit" = "Recruit"; + +/* stats.py: Federation rank; In files: stats.py:156; */ +"Cadet" = "Cadet"; + +/* stats.py: Federation rank; In files: stats.py:157; */ +"Midshipman" = "Midshipman"; + +/* stats.py: Federation rank; In files: stats.py:158; */ +"Petty Officer" = "Petty Officer"; + +/* stats.py: Federation rank; In files: stats.py:159; */ +"Chief Petty Officer" = "Chief Petty Officer"; + +/* stats.py: Federation rank; In files: stats.py:160; */ +"Warrant Officer" = "Warrant Officer"; + +/* stats.py: Federation rank; In files: stats.py:161; */ +"Ensign" = "Ensign"; + +/* stats.py: Federation rank; In files: stats.py:162; */ +"Lieutenant" = "Lieutenant"; + +/* stats.py: Federation rank; In files: stats.py:163; */ +"Lieutenant Commander" = "Lieutenant Commander"; + +/* stats.py: Federation rank; In files: stats.py:164; */ +"Post Commander" = "Post Commander"; + +/* stats.py: Federation rank; In files: stats.py:165; */ +"Post Captain" = "Post Captain"; + +/* stats.py: Federation rank; In files: stats.py:166; */ +"Rear Admiral" = "Rear Admiral"; + +/* stats.py: Federation rank; In files: stats.py:167; */ +"Vice Admiral" = "Vice Admiral"; + +/* stats.py: Federation rank; In files: stats.py:168; */ +"Admiral" = "Admiral"; + +/* stats.py: Empire rank; In files: stats.py:174; */ +"Outsider" = "Outsider"; + +/* stats.py: Empire rank; In files: stats.py:175; */ +"Serf" = "Serf"; + +/* stats.py: Empire rank; In files: stats.py:177; */ +"Squire" = "Squire"; + +/* stats.py: Empire rank; In files: stats.py:178; */ +"Knight" = "Knight"; + +/* stats.py: Empire rank; In files: stats.py:179; */ +"Lord" = "Lord"; + +/* stats.py: Empire rank; In files: stats.py:180; */ +"Baron" = "Baron"; + +/* stats.py: Empire rank; In files: stats.py:181; */ +"Viscount" = "Viscount"; + +/* stats.py: Empire rank; In files: stats.py:182; */ +"Count" = "Count"; + +/* stats.py: Empire rank; In files: stats.py:183; */ +"Earl" = "Earl"; + +/* stats.py: Empire rank; In files: stats.py:184; */ +"Marquis" = "Marquis"; + +/* stats.py: Empire rank; In files: stats.py:185; */ +"Duke" = "Duke"; + +/* stats.py: Empire rank; In files: stats.py:186; */ +"Prince" = "Prince"; + +/* stats.py: Empire rank; In files: stats.py:187; */ +"King" = "King"; + +/* stats.py: Power rank; In files: stats.py:193; */ +"Rating 1" = "Reyting 1"; + +/* stats.py: Power rank; In files: stats.py:194; */ +"Rating 2" = "Reyting 2"; + +/* stats.py: Power rank; In files: stats.py:195; */ +"Rating 3" = "Reyting 3"; + +/* stats.py: Power rank; In files: stats.py:196; */ +"Rating 4" = "Reyting 4"; + +/* stats.py: Power rank; In files: stats.py:197; */ +"Rating 5" = "Reyting 5"; + +/* stats.py: Current commander unknown when trying to use 'File' > 'Status'; In files: stats.py:315; */ +"Status: Don't yet know your Commander name" = "Durum: Cmdr adÄąnÄązÄą henÃŧz bilmiyorum"; + +/* stats.py: No Frontier CAPI data yet when trying to use 'File' > 'Status'; In files: stats.py:323; */ +"Status: No CAPI data yet" = "Durum: HenÃŧz CAPI verisi yok"; + +/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:409; */ +"Value" = "Değer"; + +/* stats.py: Status dialog title; In files: stats.py:418; */ +"Ships" = "Gemiler"; + +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} sÃŧrÃŧm mevcut"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "Stabil"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "Takibi GÃŧncelle"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "Aktif Takip {TRACK} olarak gÃŧncellendi."; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "GÃŧncelleme takibi Beta'dan KararlÄą olarak değiştirildi. ArtÄąk Beta gÃŧncellemelerini almayacaksÄąnÄąz. Bir sonraki KararlÄą sÃŧrÃŧme kadar mevcut Beta sÃŧrÃŧmÃŧnÃŧzde kalacaksÄąnÄąz.\n\nEn son Stabil sÃŧrÃŧme manuel olarak geri dÃļnebilirsiniz. Bunu yapmak için en son Stabil sÃŧrÃŧmÃŧnÃŧ manuel olarak indirip yÃŧklemeniz gerekir. Önemli değişiklikler içeren ana sÃŧrÃŧmler arasÄąnda sÃŧrÃŧm dÃŧşÃŧrme durumunda bunun hatalara yol açabileceğini veya tamamen bozulabileceğini unutmayÄąn.\n\nEn son sÃŧrÃŧmÃŧ indirmek için GitHub'u açmak istiyor musunuz?"; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "Metin dÄąÅŸÄą içerik yapÄąÅŸtÄąrÄąlamadÄą"; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Şurada aç {URL}"; + diff --git a/L10n/uk.strings b/L10n/uk.strings index 35d2c229..23febdef 100644 --- a/L10n/uk.strings +++ b/L10n/uk.strings @@ -1,9 +1,23 @@ +/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */ +"Send flight log and CMDR status to EDSM" = "Đ’Ņ–Đ´ĐŋŅ€Đ°Đ˛ĐģŅŅ‚Đ¸ даĐŊŅ– ĐąĐžŅ€Ņ‚ĐžĐ˛ĐžĐŗĐž ĐļŅƒŅ€ĐŊаĐģ҃ Đ´Đž EDSM"; + +/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */ +"Open Log Folder" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŋаĐŋĐē҃ ĐļŅƒŅ€ĐŊаĐģ҃"; + +/* inara.py:Text Inara Show API key; In files: inara.py:305; */ +"Show API Key" = "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API"; /* Language name */ "!Language" = "ĐŖĐēŅ€Đ°Ņ—ĐŊҁҌĐēа"; +/* companion.py: Frontier CAPI didn't respond; In files: companion.py:226; */ +"Error: Frontier CAPI didn't respond" = "ПоĐŧиĐģĐēа: Frontier CAPI ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ”"; + /* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:245; */ "Error: Frontier server is lagging" = "ПоĐŧиĐģĐēа: З`Ņ”Đ´ĐŊаĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ ĐŗŅ€Đ¸ С ĐˇĐ°Ņ‚Ņ€Đ¸ĐŧĐēаĐŧи!"; +/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:261; */ +"Docked but unknown station: EDO Settlement?" = "ĐŸŅ€Đ¸ŅŅ‚Đ¸ĐēОваĐŊиК, ĐŋŅ€ĐžŅ‚Đĩ ŅŅ‚Đ°ĐŊŅ†Ņ–Ņ ĐŊĐĩĐ˛Ņ–Đ´ĐžĐŧа: ĐŋĐžŅĐĩĐģĐĩĐŊĐŊŅ EDO?"; + /* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:271; */ "Error: Invalid Credentials" = "ПоĐŧиĐģĐēа: НĐĩĐ˛Ņ–Ņ€ĐŊŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– даĐŊŅ–!"; @@ -22,6 +36,18 @@ /* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */ "Error: unable to get token" = "ПоĐŧиĐģĐēа: ĐŊĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Ņ‚ĐžĐēĐĩĐŊ"; +/* EDMarketConnector.py: EDMC Critical Error Notification; */ +"EDMC encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDMC Đ˛Đ¸ŅĐ˛Đ¸Đ˛ ĐēŅ€Đ¸Ņ‚Đ¸Ņ‡ĐŊ҃ ĐŋĐžĐŧиĐģĐē҃, Ņ– ĐŊĐĩ ĐŧĐžĐļĐĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ„ŅƒĐŊĐēŅ†Ņ–ĐžĐŊŅƒĐ˛Đ°ĐŊĐŊŅ. EDMC виĐŧĐēĐŊĐĩŅ‚ŅŒŅŅ ĐˇĐ°Ņ€Đ°Đ´Đ¸ ŅĐ°ĐŧĐžĐˇĐ°Ņ…Đ¸ŅŅ‚Ņƒ!"; + +/* EDMarketConnector.py: EDMC Critical Error Details; */ +"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "ĐžŅŅŒ Ņ‰Đž EDMC СĐŧŅ–Đŗ С'ŅŅŅƒĐ˛Đ°Ņ‚Đ¸:\n\n{ERR}\n\nĐĨĐžŅ‡ĐĩŅ‚Đĩ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąĐ°Đŗ Ņ€ĐĩĐŋĐžŅ€Ņ‚ ĐŊа GitHub?"; + +/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */ +"Frontier CAPI down for maintenance" = "Frontier CAPI Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊиК ҇ĐĩŅ€ĐĩС Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐĩ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ"; + +/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:856; */ +"Frontier CAPI query failure" = "ПоĐŧиĐģĐēа СаĐŋĐ¸Ņ‚Ņƒ Frontier CAPI"; + /* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */ "Update" = "ОĐŊОвĐģĐĩĐŊĐŊŅ"; @@ -31,6 +57,9 @@ /* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:837; */ "Unknown" = "НĐĩĐ˛Ņ–Đ´ĐžĐŧĐž"; +/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:906; */ +"Error: Check E:D journal file location" = "ПоĐŧиĐģĐēа: ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃ ĐļŅƒŅ€ĐŊаĐģ҃ E:D"; + /* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */ "Cmdr" = "КĐŧ-Đ´Ņ€"; @@ -55,12 +84,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "Вид"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "Đ’Ņ–ĐēĐŊĐž"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "Đ”ĐžĐ˛Ņ–Đ´Đēа"; @@ -70,6 +93,9 @@ /* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */ "Check for Updates..." = "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēа ĐžĐŊОвĐģĐĩĐŊҌ..."; +/* EDMarketConnector.py: Help > Open System Profiler; In files: EDMarketConnector.py:888; */ +"Open System Profiler" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŸŅ€ĐžŅ„Đ°ĐšĐģĐĩŅ€ ĐĄĐ¸ŅŅ‚ĐĩĐŧ"; + /* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */ "Save Raw Data..." = "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊĐĩĐžĐąŅ€ĐžĐąĐģĐĩĐŊŅ– даĐŊŅ–..."; @@ -79,6 +105,12 @@ /* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:933; EDMarketConnector.py:953; */ "Documentation" = "ДоĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ"; +/* EDMarketConnector.py: Help > Troubleshooting; In files: EDMarketConnector.py:934; EDMarketConnector.py:954; */ +"Troubleshooting" = "ĐŖŅŅƒĐŊĐĩĐŊĐŊŅ ĐŊĐĩҁĐŋŅ€Đ°Đ˛ĐŊĐžŅŅ‚ĐĩĐš"; + +/* EDMarketConnector.py: Help > Report A Bug; In files: EDMarketConnector.py:935; EDMarketConnector.py:955; */ +"Report A Bug" = "ĐŸĐžĐ˛Ņ–Đ´ĐžĐŧĐ¸Ņ‚Đ¸ ĐŋŅ€Đž ĐŋĐžĐŧиĐģĐē҃"; + /* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:936; EDMarketConnector.py:956; */ "Privacy Policy" = "ПоĐģŅ–Ņ‚Đ¸Đēа ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊĐžŅŅ‚Ņ–"; @@ -94,6 +126,18 @@ /* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */ "Copy" = "КоĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸"; +/* myNotebook.py: Label for 'Cut' as in 'Cut and Paste'; */ +"Cut" = "Đ’Đ¸Ņ€Ņ–ĐˇĐ°Ņ‚Đ¸"; + +/* myNotebook.py: Label for 'Paste' as in 'Copy and Paste'; */ +"Paste" = "Đ’ŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸"; + +/* myNotebook.py: Label for 'Select All'; */ +"Select All" = "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅĐĩ"; + +/* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */ +"CAPI auth disabled by killswitch" = "ĐĐ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ CAPI виĐēĐģŅŽŅ‡ĐĩĐŊа Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ”ŅŽ Đ°Đ˛Đ°Ņ€Ņ–ĐšĐŊĐžĐŗĐž Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ"; + /* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:978; */ "Logging in..." = "Đ’Ņ…Ņ–Đ´ в..."; @@ -109,9 +153,36 @@ /* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:1038; */ "Station doesn't have a market!" = "На ŅŅ‚Đ°ĐŊ҆Җҗ ĐŊĐĩĐŧĐ°Ņ” Ņ€Đ¸ĐŊĐē҃!"; +/* EDMarketConnector.py: CAPI queries aborted because Cmdr name is unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because Cmdr name is unknown; In files: EDMarketConnector.py:1077; EDMarketConnector.py:1164; */ +"CAPI query aborted: Cmdr name unknown" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: НĐĩĐ˛Ņ–Đ´ĐžĐŧĐĩ Ņ–Đŧ'Ņ КМДР"; + +/* EDMarketConnector.py: CAPI queries aborted because game mode unknown; In files: EDMarketConnector.py:1083; */ +"CAPI query aborted: Game mode unknown" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: НĐĩĐ˛Ņ–Đ´ĐžĐŧиК Ņ€ĐĩĐļиĐŧ ĐŗŅ€Đ¸"; + +/* EDMarketConnector.py: CAPI queries aborted because GameVersion unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because GameVersion unknown; In files: EDMarketConnector.py:1089; EDMarketConnector.py:1170; */ +"CAPI query aborted: GameVersion unknown" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: НĐĩĐ˛Ņ–Đ´ĐžĐŧа вĐĩŅ€ŅŅ–Ņ ĐŗŅ€Đ¸"; + +/* EDMarketConnector.py: CAPI queries aborted because current star system name unknown; In files: EDMarketConnector.py:1095; */ +"CAPI query aborted: Current system unknown" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: НĐĩĐ˛Ņ–Đ´ĐžĐŧа ĐŋĐžŅ‚ĐžŅ‡ĐŊа ŅĐ¸ŅŅ‚ĐĩĐŧа"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in multi-crew on other Cmdr's ship; In files: EDMarketConnector.py:1101; */ +"CAPI query aborted: In other-ship multi-crew" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: в Ņ€ĐĩĐļиĐŧŅ– Đŧ҃ĐģŅŒŅ‚Đ¸ĐĩĐēŅ–ĐŋаĐļ҃ Ņ–ĐŊŅˆĐžĐŗĐž ĐēĐžŅ€Đ°ĐąĐģŅ"; + +/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:1107; */ +"CAPI query aborted: CQC (Arena) detected" = "ЗаĐŋĐ¸Ņ‚ CAPI ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК: ĐŅ€ĐĩĐŊа БĐģĐ¸ĐˇŅŒĐēĐžĐŗĐž ĐąĐžŅŽ (CQC) Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊа"; + /* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; In files: EDMarketConnector.py:1128; EDMarketConnector.py:1179; */ "Fetching data..." = "ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ даĐŊĐ¸Ņ…..."; +/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ +"CAPI fleetcarrier disabled by killswitch" = "ЗаĐŋĐ¸Ņ‚ CAPI ĐēĐžŅ€Đ°ĐąĐģŅ-ĐŊĐžŅŅ–Ņ ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊиК Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ”ŅŽ Đ°Đ˛Đ°Ņ€Ņ–ĐšĐŊĐžĐŗĐž Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ"; + +/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ +"CAPI: No Fleet Carrier data returned" = "CAPI: НĐĩĐŧĐ°Ņ” даĐŊĐ¸Ņ… ĐēĐžŅ€Đ°ĐąĐģŅ-ĐŊĐžŅŅ–Ņ"; + +/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */ +"CAPI: Fleet Carrier data incomplete" = "CAPI: ДаĐŊŅ– ĐēĐžŅ€Đ°ĐąĐģŅ-ĐŊĐžŅŅ–Ņ ĐŊĐĩĐŋОвĐŊŅ–"; + /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPI: НĐĩ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐž даĐŊĐŊĐ¸Ņ… ĐŋŅ–ĐģĐžŅ‚Đ°"; @@ -124,6 +195,12 @@ /* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1259; stats.py:349; */ "What are you flying?!" = "На Ņ‡ĐžĐŧ҃ ĐģĐĩŅ‚Đ¸ĐŧĐž?!"; +/* EDMarketConnector.py: Frontier CAPI server error when fetching data; In files: EDMarketConnector.py:1384; */ +"Frontier CAPI server error" = "Frontier CAPI: cĐĩŅ€Đ˛ĐĩŅ€ĐŊа ĐŋĐžĐŧиĐģĐēа"; + +/* EDMarketConnector.py: Frontier CAPI Access Token expired, trying to get a new one; In files: EDMarketConnector.py:1390; */ +"CAPI: Refreshing access token..." = "CAPI: ОĐŊОвĐģŅŽŅ”ĐŧĐž Ņ‚ĐžĐēĐĩĐŊ Đ´ĐžŅŅ‚ŅƒĐŋ҃..."; + /* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1434; */ "Last updated at %H:%M:%S" = "ĐžŅŅ‚Đ°ĐŊĐŊŅ” ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐąŅƒĐģĐž %H:%M:%S"; @@ -154,6 +231,24 @@ /* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */ "EDMC: Plugins Without Python 3.x Support" = "EDMC: ПĐģĐ°ĐŗŅ–ĐŊи ĐąĐĩС ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи Python 3.x!"; +/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */ +"EDMC: Broken Plugins" = "EDMC: ПоĐģĐžĐŧаĐŊŅ– ĐŋĐģĐ°ĐŗŅ–ĐŊи"; + +/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */ +"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ОдиĐŊ айО Đ´ĐĩĐēŅ–ĐģҌĐēа ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐ¸Ņ… ĐŋĐģĐ°ĐŗŅ–ĐŊŅ–Đ˛. ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅŒŅ‚Đĩ ҁĐŋĐ¸ŅĐžĐē ĐŊа вĐēĐģĐ°Đ´Ņ†Ņ– '{PLUGINS}' в '{FILE}' > '{SETTINGS}'. ĐĻŅ ĐŋĐžĐŧиĐģĐēа ĐŧĐžĐŗĐģа ĐąŅƒŅ‚Đ¸ ҁĐŋŅ€Đ¸Ņ‡Đ¸ĐŊĐĩĐŊа ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐžŅŽ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ ĐŋаĐŋĐžĐē. ФаКĐģ load.py ĐŋОвиĐŊĐĩĐŊ СĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đ¸ŅŅ ҃ ĐŋаĐŋ҆Җ plugins/PLUGIN_NAME/load.py\n\nВи ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ ĐŋĐģĐ°ĐŗŅ–ĐŊ, ĐŋĐĩŅ€ĐĩĐšĐŧĐĩĐŊŅƒĐ˛Đ°Đ˛ŅˆĐ¸ ĐšĐžĐŗĐž ĐŋаĐŋĐē҃ Đ´ĐžĐ´Đ°Đ˛ŅˆĐ¸ '{DISABLED}' в ĐēŅ–ĐŊ҆Җ ĐŊаСви."; + +/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */ +"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "ОдиĐŊ айО Đ´ĐĩĐēŅ–ĐģҌĐēа Đ˛Đ°ŅˆĐ¸Ņ… ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€Ņ–Đ˛ URL ĐąŅƒĐģи Ņ…Đ¸ĐąĐŊŅ– Ņ‚Đ° ҁĐēиĐŊŅƒŅ‚Ņ–:\n"; + +/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */ +"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} Đŧав СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ {OLDPROV}, Ņ‚Đ° ĐąŅƒĐ˛ ҁĐēиĐŊŅƒŅ‚Đ¸Đš Đ´Đž {NEWPROV}\n"; + +/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */ +"EDMC: Default Providers Reset" = "EDMC: ĐŸŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€Đ¸ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ĐąŅƒĐģи ҁĐēиĐŊŅƒŅ‚Ņ–"; + +/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */ +"Awaiting Full CMDR Login" = "ĐžŅ‡Ņ–ĐēŅƒŅ”ĐŧĐž ĐŋОвĐŊĐžĐŗĐž Đ˛Ņ…ĐžĐ´Ņƒ КМДР"; + /* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */ "Journal directory already locked" = "ĐšĐ°Ņ‚Đ°ĐģĐžĐŗ ĐļŅƒŅ€ĐŊаĐģ҃ вĐļĐĩ СайĐģĐžĐēОваĐŊиК"; @@ -169,9 +264,45 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "ĐĄŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐŊĐĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ"; +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ +"Auto" = "ĐĐ˛Ņ‚Đž"; + +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ +"Normal" = "ĐĐžŅ€ĐŧаĐģҌĐŊиК"; + +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ +"Beta" = "БĐĩŅ‚Đ°"; + +/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ +"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Đ’ŅŅ‚Đ°Đ˛Ņ‚Đĩ URL Đ´ĐģŅ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ Ņ–Đˇ ĐēĐžŅ€Đ°ĐąĐĩĐģҌĐŊиĐŧи ĐˇĐąŅ–Ņ€ĐēаĐŧи coriolis.io. Đ—Đ°ŅƒĐ˛Đ°ĐļŅ‚Đĩ Ņ‰Đž '/import?data=' ПОВИННО ĐąŅƒŅ‚Đ¸ в ĐēŅ–ĐŊ҆Җ!"; + +/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: coriolis.py:97; */ +"Normal URL" = "ĐĐžŅ€ĐŧаĐģҌĐŊиК URL"; + +/* coriolis.py: Generic 'Reset' button label; In files: coriolis.py:100; coriolis.py:109; */ +"Reset" = "ĐĄĐēиĐŊŅƒŅ‚Đ¸"; + +/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: coriolis.py:106; */ +"Beta URL" = "БĐĩŅ‚Đ° URL"; + +/* coriolis.py: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL; In files: coriolis.py:116; */ +"Override Beta/Normal Selection" = "ПĐĩŅ€ĐĩвиСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ БĐĩŅ‚Đ°/ĐĐžŅ€ĐŧаĐģҌĐŊиК"; + +/* coriolis.py: Settings>Coriolis - invalid override mode found; In files: coriolis.py:156; */ +"Invalid Coriolis override mode!" = "НĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК Ņ€ĐĩĐļиĐŧ ĐŋĐĩŅ€ĐĩвиСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Coriolis!"; + /* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:458; eddn.py:2413; eddn.py:2451; eddn.py:2519; */ "Error: Can't connect to EDDN" = "ПоĐŧиĐģĐēа: НĐĩĐŧĐ°Ņ” Св'ŅĐˇĐē҃ С EDDN!"; +/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:576; */ +"EDDN Error: EDMC is too old for EDDN. Please update." = "ПоĐŧиĐģĐēа EDDN: вĐĩŅ€ŅŅ–Ņ EDMC Ņ” СаĐŊĐ°Đ´Ņ‚Đž ŅŅ‚Đ°Ņ€ĐžŅŽ Đ´ĐģŅ EDDN. Đ‘ŅƒĐ´ŅŒ-ĐģĐ°ŅĐēа, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ."; + +/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:582; */ +"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "ПоĐŧиĐģĐēа EDDN: ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēа ĐŋŅ€ĐžĐ˛Đ°ĐģĐĩĐŊа (EDMC СаĐŊадĐŊĐž ŅŅ‚Đ°Ņ€Đ¸Đš?). Див. Đ›ĐžĐŗ"; + +/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:587; */ +"EDDN Error: Returned {STATUS} status code" = "ПоĐŧиĐģĐēа EDDN: Код {STATUS} ĐąŅƒĐģĐž ĐŋОвĐĩŅ€ĐŊŅƒŅ‚Đž."; + /* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:2041; */ "Send station data to the Elite Dangerous Data Network" = "ĐĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ даĐŊŅ– ŅŅ‚Đ°ĐŊŅ†Ņ–Đš Đ´Đž Elite Dangerous Data Network"; @@ -181,6 +312,9 @@ /* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:2063; */ "Delay sending until docked" = "Đ’Ņ–Đ´ĐēĐģĐ°ŅŅ‚Đ¸ Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛Đē҃ даĐŊĐ¸Ņ… Đ´Đž ŅŅ‚Đ¸ĐēŅƒĐ˛Đ°ĐŊĐŊŅ"; +/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:2178; */ +"EDDN journal handler disabled. See Log." = "Đ–ŅƒŅ€ĐŊаĐģ EDDN Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž. Див. Đ›ĐžĐŗ"; + /* eddn.py: Status text shown while attempting to send data; In files: eddn.py:2507; */ "Sending data to EDDN..." = "Đ’Ņ–Đ´ĐŋŅ€Đ°Đ˛Đēа даĐŊĐ¸Ņ… Đ´Đž EDDN..."; @@ -196,6 +330,12 @@ /* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:394; prefs.py:527; prefs.py:1157; prefs.py:1190; stats.py:154; stats.py:173; stats.py:192; stats.py:209; */ "None" = "ĐŅ–Ņ‡ĐžĐŗĐž"; +/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:516; */ +"EDSM Handler disabled. See Log." = "ĐžĐąŅ€ĐžĐąĐŊиĐē EDSM Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž. Див. Đ›ĐžĐŗ"; + +/* edsm.py: EDSM - Only Live data; In files: edsm.py:632; */ +"EDSM only accepts Live galaxy data" = "EDSM ĐŋŅ€Đ¸ĐšĐŧĐ°Ņ” даĐŊŅ– ҂ҖĐģҌĐēи Live вĐĩҀҁҖҗ ĐŗĐ°ĐģаĐēŅ‚Đ¸Đēи"; + /* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:916; edsm.py:1048; */ "Error: EDSM {MSG}" = "ПоĐŧиĐģĐēа: EDSM {MSG}!"; @@ -208,12 +348,15 @@ /* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:269; */ "Inara credentials" = "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ Inara"; +/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; inara.py: Inara - Only Live data; In files: inara.py:384; inara.py:386; */ +"Inara only accepts Live galaxy data" = "Inara ĐŋŅ€Đ¸ĐšĐŧĐ°Ņ” даĐŊŅ– ҂ҖĐģҌĐēи Live вĐĩҀҁҖҗ ĐŗĐ°ĐģаĐēŅ‚Đ¸Đēи"; + +/* inara.py: INARA support disabled via killswitch; In files: inara.py:395; */ +"Inara disabled. See Log." = "Inara Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž. Див. Đ›ĐžĐŗ"; + /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "ПоĐŧиĐģĐēа: Inara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "ĐŖĐŋОдОйаĐŊĐŊŅ"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "ОбĐĩŅ€Ņ–Ņ‚ŅŒ даĐŊŅ– ŅĐēŅ– ĐąŅƒĐ´Đĩ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž"; @@ -232,9 +375,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "Đ ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "ВĐŊĐĩҁĐĩĐŊĐŊŅ СĐŧŅ–ĐŊ..."; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "ПĐĩŅ€ĐĩĐŗĐģŅĐ´..."; @@ -244,21 +384,15 @@ /* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */ "E:D journal file location" = "Đ ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃-ĐļŅƒŅ€ĐŊаĐģ҃ E:D"; -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "ĐĄĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐŊŅ ĐēĐģĐ°Đ˛Ņ–Ņˆ"; +/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */ +"CAPI Settings" = "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ CAPI"; + +/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ +"Enable Fleet Carrier CAPI Queries" = "Đ’Đ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ СаĐŋĐ¸Ņ‚Đ¸ CAPI ĐēĐžŅ€Đ°ĐąĐģŅ–Đ˛-ĐŊĐžŅŅ–Ņ—Đ˛"; /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "Đ“Đ°Ņ€ŅŅ‡Đ° ĐēĐģĐ°Đ˛Ņ–ŅˆĐ°"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "ПĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅ‚Đĩ {APP} Đ´ĐģŅ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ŅŅ€ĐģиĐēŅ–Đ˛"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊŅ– дОСвОĐģи Đ´ĐģŅ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ŅŅ€ĐģиĐēŅ–Đ˛"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐ¸ŅŅ‚ĐĩĐŧи"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "ĐĸŅ–ĐģҌĐēи Ņ‚ĐžĐ´Ņ–, ĐēĐžĐģи Elite: Dangerous Ņ” аĐēŅ‚Đ¸Đ˛ĐŊиĐŧ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐŧ"; @@ -283,6 +417,15 @@ /* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:681; */ "Configuration" = "КоĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ"; +/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:690; */ +"Main UI privacy options" = "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ¸Đ˛Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– ĐžŅĐŊОвĐŊĐžĐŗĐž Đ˛Ņ–ĐēĐŊа."; + +/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:695; */ +"Hide private group name in UI" = "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ ĐŋŅ€Đ¸Đ˛Đ°Ņ‚ĐŊĐžŅ— ĐŗŅ€ŅƒĐŋи ҃ Đ˛Ņ–ĐēĐŊŅ–"; + +/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:699; */ +"Hide multi-crew captain name" = "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ–Đŧ'Ņ ĐēаĐŋŅ–Ņ‚Đ°ĐŊа Đŧ҃ĐģŅŒŅ‚Đ¸ĐĩĐēŅ–ĐŋаĐļ҃"; + /* prefs.py: Preferences privacy tab title; In files: prefs.py:703; */ "Privacy" = "КоĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ"; @@ -325,8 +468,11 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "ĐĄŅ…ĐžĐ˛Đ¸Ņ‰Đĩ ĐŋĐģĐ°ĐŗŅ–ĐŊŅ–Đ˛"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸"; +/* prefs.py: Label on button used to open the Plugin Folder; */ +"Open Plugins Folder" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŋаĐŋĐē҃ ĐŋĐģĐ°ĐŗŅ–ĐŊŅ–Đ˛"; + +/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */ +"Plugin Directory Location" = "ĐœŅ–ŅŅ†ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐŋаĐŋĐēи ĐŋĐģĐ°ĐŗŅ–ĐŊŅ–Đ˛"; /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "ĐŸŅ–Đ´ĐēаСĐēа: Ви ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ ĐŋĐģĐ°ĐŗŅ–ĐŊ ҇ĐĩŅ€ĐĩС {CR} дОдаĐŊĐŊŅ '{EXT}' Đ´Đž ĐŊаСви ĐŋаĐŋĐēи"; @@ -340,9 +486,15 @@ /* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */ "Information on migrating plugins" = "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž ĐŧŅ–ĐŗŅ€Đ°Ņ†Ņ–ŅŽ ĐŋĐģĐ°ĐŗŅ–ĐŊŅ–Đ˛"; +/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */ +"Broken Plugins" = "ПоĐģĐžĐŧаĐŊŅ– ĐŋĐģĐ°ĐŗŅ–ĐŊи"; + /* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */ "Disabled Plugins" = "ВиĐŧĐēĐŊĐĩĐŊŅ– ĐŋĐģĐ°ĐŗŅ–ĐŊи"; +/* prefs.py: Catch & Record Profiler Errors; */ +"Error in System Profiler" = "ПоĐŧиĐģĐēа ҃ ĐŸŅ€ĐžŅ„Đ°ĐšĐģĐĩҀҖ ĐĄĐ¸ŅŅ‚ĐĩĐŧ"; + /* stats.py: Cmdr stats; In files: stats.py:58; */ "Balance" = "БаĐģаĐŊҁ"; @@ -352,6 +504,21 @@ /* stats.py: Top rank; In files: stats.py:63; */ "Elite" = "ЕĐģŅ–Ņ‚Đ°"; +/* stats.py: Top rank +1; In files: stats.py:64; */ +"Elite I" = "ЕĐģŅ–Ņ‚Đ° І"; + +/* stats.py: Top rank +2; In files: stats.py:65; */ +"Elite II" = "ЕĐģŅ–Ņ‚Đ° ІІ"; + +/* stats.py: Top rank +3; In files: stats.py:66; */ +"Elite III" = "ЕĐģŅ–Ņ‚Đ° ІІІ"; + +/* stats.py: Top rank +4; In files: stats.py:67; */ +"Elite IV" = "ЕĐģŅ–Ņ‚Đ° IV"; + +/* stats.py: Top rank +5; In files: stats.py:68; */ +"Elite V" = "ЕĐģŅ–Ņ‚Đ° V"; + /* stats.py: Ranking; In files: stats.py:74; */ "Combat" = "Бойовий"; @@ -361,6 +528,12 @@ /* stats.py: Ranking; In files: stats.py:76; */ "Explorer" = "Đ”ĐžŅĐģŅ–Đ´ĐŊĐ¸Ņ†ŅŒĐēиК"; +/* stats.py: Ranking; In files: stats.py:77; */ +"Mercenary" = "НайĐŧаĐŊĐĩŅ†ŅŒ"; + +/* stats.py: Ranking; In files: stats.py:78; */ +"Exobiologist" = "ЕĐēĐˇĐžĐąŅ–ĐžĐģĐžĐŗ"; + /* stats.py: Ranking; In files: stats.py:79; */ "CQC" = "БĐģĐ¸ĐˇŅŒĐēиК ĐąŅ–Đš (CQC)"; @@ -445,6 +618,51 @@ /* stats.py: Explorer rank; In files: stats.py:118; */ "Pioneer" = "ĐŸŅ–ĐžĐŊĐĩŅ€"; +/* stats.py: Mercenary rank; In files: stats.py:122; */ +"Defenceless" = "БĐĩĐˇĐˇĐ°Ņ…Đ¸ŅĐŊиК"; + +/* stats.py: Mercenary rank; In files: stats.py:123; */ +"Mostly Defenceless" = "ЗдĐĩĐąŅ–ĐģŅŒŅˆĐžĐŗĐž ĐąĐĩĐˇĐˇĐ°Ņ…Đ¸ŅĐŊиК"; + +/* stats.py: Mercenary rank; In files: stats.py:124; */ +"Rookie" = "ĐĐžĐ˛Đ°Ņ‡ĐžĐē"; + +/* stats.py: Mercenary rank; In files: stats.py:125; */ +"Soldier" = "ĐĄĐžĐģĐ´Đ°Ņ‚"; + +/* stats.py: Mercenary rank; In files: stats.py:126; stats.py:128; */ +"Gunslinger" = "ĐĄŅ‚Ņ€Ņ–ĐģĐĩŅ†ŅŒ"; + +/* stats.py: Mercenary rank; In files: stats.py:127; */ +"Warrior" = "Đ’ĐžŅ—ĐŊ"; + +/* stats.py: Mercenary rank; In files: stats.py:129; */ +"Deadeye" = "ĐĄĐŊаКĐŋĐĩŅ€"; + +/* stats.py: Exobiologist rank; In files: stats.py:132; */ +"Directionless" = "БĐĩСĐŊаĐŋŅ€ŅĐŧĐŊиК"; + +/* stats.py: Exobiologist rank; In files: stats.py:133; */ +"Mostly Directionless" = "ЗдĐĩĐąŅ–ĐģŅŒŅˆĐžĐŗĐž ĐąĐĩСĐŊаĐŋŅ€ŅĐŧĐŊиК"; + +/* stats.py: Exobiologist rank; In files: stats.py:134; */ +"Compiler" = "ĐŖĐŋĐžŅ€ŅĐ´ĐŊиĐē"; + +/* stats.py: Exobiologist rank; In files: stats.py:135; */ +"Collector" = "КоĐģĐĩĐēŅ‚ĐžŅ€"; + +/* stats.py: Exobiologist rank; In files: stats.py:136; */ +"Cataloguer" = "ĐšĐ°Ņ‚Đ°ĐģĐžĐŗŅ–ŅŅ‚"; + +/* stats.py: Exobiologist rank; In files: stats.py:137; */ +"Taxonomist" = "ĐĄĐ¸ŅŅ‚ĐĩĐŧĐ°Ņ‚Đ¸Đē"; + +/* stats.py: Exobiologist rank; In files: stats.py:138; */ +"Ecologist" = "ЕĐēĐžĐģĐžĐŗ"; + +/* stats.py: Exobiologist rank; In files: stats.py:139; */ +"Geneticist" = "ГĐĩĐŊĐĩŅ‚Đ¸Đē"; + /* stats.py: CQC rank; In files: stats.py:142; */ "Helpless" = "БĐĩСĐŋĐžŅ€Đ°Đ´ĐŊиК"; @@ -565,9 +783,44 @@ /* stats.py: Power rank; In files: stats.py:197; */ "Rating 5" = "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ 5"; +/* stats.py: Current commander unknown when trying to use 'File' > 'Status'; In files: stats.py:315; */ +"Status: Don't yet know your Commander name" = "ĐĄŅ‚Đ°Ņ‚ŅƒŅ: ПоĐēи ҉Đĩ ĐŊĐĩ СĐŊĐ°Ņ”ĐŧĐž Ņ–Đŧ'Ņ Đ˛Đ°ŅˆĐžĐŗĐž КМДР"; + +/* stats.py: No Frontier CAPI data yet when trying to use 'File' > 'Status'; In files: stats.py:323; */ +"Status: No CAPI data yet" = "ĐĄŅ‚Đ°Ņ‚ŅƒŅ: ПоĐēи ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” даĐŊĐ¸Ņ… CAPI"; + /* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:409; */ "Value" = "Đ’Đ°Ņ€Ņ‚Ņ–ŅŅ‚ŅŒ"; /* stats.py: Status dialog title; In files: stats.py:418; */ "Ships" = "ĐšĐžŅ€Đ°ĐąĐģŅ–"; +/* update.py: Update Available Text; In files: update.py:229; */ +"{NEWVER} is available" = "{NEWVER} Đ´ĐžŅŅ‚ŅƒĐŋĐŊа"; + +/* prefs.py: Stable Version of EDMC; */ +"Stable" = "ĐĄŅ‚Đ°ĐąŅ–ĐģҌĐŊиК"; + +/* prefs.py: Select the Update Track (Beta, Stable); */ +"Update Track" = "ШĐģŅŅ… ĐžĐŊОвĐģĐĩĐŊҌ"; + +/* EDMarketConnector.py: Inform the user the Update Track has changed; */ +"Update Track Changed to {TRACK}" = "ШĐģŅŅ… ĐžĐŊОвĐģĐĩĐŊҌ СĐŧŅ–ĐŊĐĩĐŊĐž ĐŊа {TRACK}"; + +/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */ +"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "ШĐģŅŅ… ĐžĐŊОвĐģĐĩĐŊҌ СĐŧŅ–ĐŊĐĩĐŊĐž С \"БĐĩŅ‚Đ°\" ĐŊа \"ĐĄŅ‚Đ°ĐąŅ–ĐģҌĐŊиК\". Ви ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ ĐąŅƒĐ´ĐĩŅ‚Đĩ ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ĐąĐĩŅ‚Đ° ĐžĐŊОвĐģĐĩĐŊҌ. Ви СаĐģĐ¸ŅˆĐ¸Ņ‚ĐĩŅŅ ĐŊа вĐĩҀҁҖҗ ĐąĐĩŅ‚Đ° Đ´Đž ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž ŅŅ‚Đ°ĐąŅ–ĐģҌĐŊĐžĐŗĐž Ņ€ĐĩĐģŅ–ĐˇŅƒ.\n\nВи ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ€ŅƒŅ‡ĐŊ҃ вĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅ Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžŅ— ŅŅ‚Đ°ĐąŅ–ĐģҌĐŊĐžŅ— вĐĩҀҁҖҗ. ЊОй ҆Đĩ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸, ваĐŧ ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊĐž СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐžŅŅ‚Đ°ĐŊĐŊŅŽ ŅŅ‚Đ°ĐąŅ–ĐģҌĐŊ҃ вĐĩŅ€ŅŅ–ŅŽ Đ˛Ņ€ŅƒŅ‡ĐŊ҃. Đ—Đ°ŅƒĐ˛Đ°ĐļŅ‚Đĩ Ņ‰Đž СаĐŊиĐļĐĩĐŊĐŊŅ вĐĩҀҁҖҗ ĐŧŅ–Đļ Ņ€ĐĩĐģŅ–ĐˇĐ°Đŧи С ĐąĐ°ĐŗĐ°Ņ‚ŅŒĐŧа СĐŧŅ–ĐŊаĐŧи ĐŧĐžĐļĐĩ ҁĐŋŅ€Đ¸Ņ‡Đ¸ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐēи Ņ‡Đ¸ ĐŋОвĐŊŅ–ŅŅ‚ŅŽ ҃ĐŊĐĩĐŧĐžĐļĐģĐ¸Đ˛Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‚Ņƒ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи.\n\nБаĐļĐ°Ņ”Ņ‚Đĩ Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ GitHub Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐž Ņ€ĐĩĐģŅ–ĐˇŅƒ?"; + +/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */ +"Restart Required" = "НĐĩĐžĐąŅ…Ņ–Đ´ĐŊĐž ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ"; + +/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */ +"A restart of EDMC is required. EDMC will now restart." = "НĐĩĐžĐąŅ…Ņ–Đ´ĐŊĐž ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ EDMC. EDMC ĐąŅƒĐ´Đĩ ĐˇĐ°Ņ€Đ°Đˇ ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļĐĩĐŊиК."; + +/* myNotebook.py: Can't Paste Images or Files in Text; */ +"Cannot paste non-text content." = "НĐĩĐŧĐžĐļĐģивО Đ˛ŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐŊĐĩŅ‚ĐĩĐēŅŅ‚ĐžĐ˛Đ¸Đš вĐŧҖҁ҂ ĐąŅƒŅ„ĐĩŅ€Đ° ОйĐŧŅ–ĐŊ҃."; + +/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */ +"Open in {URL}" = "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ҃ {URL}"; + +/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */ +"Copy Inara SLEF" = "КоĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ Inara SLEF"; diff --git a/L10n/zh-Hans.strings b/L10n/zh-Hans.strings index 2b9eb75d..0aee0b8b 100644 --- a/L10n/zh-Hans.strings +++ b/L10n/zh-Hans.strings @@ -70,12 +70,6 @@ /* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */ "Edit" = "įŧ–čž‘"; -/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */ -"View" = "昞į¤ē"; - -/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */ -"Window" = "įĒ—åŖ"; - /* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */ "Help" = "帎劊"; @@ -157,12 +151,6 @@ /* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */ "CAPI fleetcarrier disabled by killswitch" = "CAPI čˆ°é˜Ÿæ¯čˆ° (fleet carrier) čĸĢ killswitch įρᔍ"; -/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */ -"CAPI: No fleetcarrier data returned" = "CAPIīŧšæ— čˆ°é˜Ÿæ¯čˆ° (fleet carrier) 数捎"; - -/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */ -"CAPI: Fleetcarrier data incomplete" = "CAPIīŧščˆ°é˜Ÿæ¯čˆ° (fleet carrier) 数捎不厌整"; - /* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */ "CAPI: No commander data returned" = "CAPIīŧšæ˛Ąæœ‰æŒ‡æŒĨ厘数捎"; @@ -226,13 +214,13 @@ /* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */ "Default" = "éģ˜čޤ"; -/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */ +/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */ "Auto" = "č‡Ē动"; -/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */ +/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */ "Normal" = "æ™Žé€šį‰ˆ"; -/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */ +/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */ "Beta" = "æĩ‹č¯•į‰ˆ"; /* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */ @@ -319,9 +307,6 @@ /* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */ "Error: Inara {MSG}" = "错蝝īŧšInara {MSG}"; -/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */ -"Preferences" = "偏åĨŊ"; - /* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */ "Please choose what data to save" = "č¯ˇé€‰æ‹Šæƒŗčρäŋå­˜įš„æ•°æŽ"; @@ -340,9 +325,6 @@ /* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */ "File location" = "äŋå­˜äŊįŊŽ"; -/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */ -"Change..." = "更攚â€Ļ"; - /* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */ "Browse..." = "æĩč§ˆâ€Ļ"; @@ -355,24 +337,9 @@ /* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */ "CAPI Settings" = "CAPI 莞įŊŽ"; -/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */ -"Enable Fleetcarrier CAPI Queries" = "åŧ€å¯čˆ°é˜Ÿæ¯čˆ° (fleet carrier) CAPI čŽŋ问"; - -/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */ -"Keyboard shortcut" = "é”Žį›˜åŋĢæˇé”Ž"; - /* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */ "Hotkey" = "åŋĢæˇé”Ž"; -/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */ -"Re-start {APP} to use shortcuts" = "重启 {APP} äģĨäŊŋᔍåŋĢæˇé”Ž"; - -/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */ -"{APP} needs permission to use shortcuts" = "{APP} 需čĻæƒé™äģĨäŊŋᔍåŋĢæˇé”Ž"; - -/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */ -"Open System Preferences" = "打åŧ€įŗģįģŸååĨŊ莞įŊŽ"; - /* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */ "Only when Elite: Dangerous is the active app" = "äģ…åŊ“ Elite: Dangerous 在前台čŋčĄŒæ—ļį”Ÿæ•ˆ"; @@ -448,9 +415,6 @@ /* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */ "Plugins folder" = "插äģļæ–‡äģļ多"; -/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */ -"Open" = "打åŧ€"; - /* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */ "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "提į¤ēīŧšæƒŗčρįĻį”¨æŸä¸€éĄšæ’äģļīŧŒ{CR}äŊ å¯äģĨæˇģ加 {EXT} 到å…ļ文äģļå¤šåį§°ä¸­"; diff --git a/PLUGINS.md b/PLUGINS.md index 3048f345..da7303f4 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -31,7 +31,7 @@ then you'll need to be using an appropriate version of Python. The current version is listed in the [Environment section of Releasing.md](https://github.com/EDCD/EDMarketConnector/blob/main/docs/Releasing.md#environment). If you're developing your plugin simply against an install of EDMarketConnector -then you'll be relying on the bundled version of Python (it's baked +then you'll be relying on the bundled version of Python (it's baked into the .exe via the py2exe build process). Please be sure to read the [Avoiding potential pitfalls](#avoiding-potential-pitfalls) @@ -40,21 +40,21 @@ EDMarketConnector code including whole application crashes. ## Being aware of core application changes -It is highly advisable to ensure you are aware of all EDMarketConnector -releases, including the pre-releases. The -beta and -rc changelogs will +It is highly advisable to ensure you are aware of all EDMarketConnector +releases, including the pre-releases. The -beta and -rc changelogs will contain valuable information about any forthcoming changes that affect plugins. The easiest way is: 1. Login to [GitHub](https://github.com). 2. Navigate to [EDMarketConnector](https://github.com/EDCD/EDMarketConnector). - 3. Click the 'Watch' (or 'Unwatch' if you previously set up any watches on - us). It's currently (2021-05-13) the left-most button of 3 near the + 3. Click the 'Watch' (or 'Unwatch' if you previously set up any watches on + us). It's currently (2021-05-13) the left-most button of 3 near the top-right of the page. 4. Click 'Custom'. 5. Ensure 'Releases' is selected. 6. Click 'Apply'. -And, of course, either ensure you check your GitHub messages regularly, or +And, of course, either ensure you check your GitHub messages regularly, or have it set up to email you such notifications. You should also keep an eye on [our GitHub Discussions](https://github.com/EDCD/EDMarketConnector/discussions) @@ -113,13 +113,13 @@ from the original files unless specified as allowed in this section. Use `monitor.game_running()` as follows in case a plugin needs to know if we think the game is running. *NB: This is a function, and should be called as such. Using the bare word `game_running` will always be `True`.* - + ``` from monitor import monitor ... if monitor.game_running(): ... -``` +``` Use `monitor.is_live_galaxy()` to determine if the player is playing in the Live galaxy. Note the implementation details of this. At time of writing it @@ -135,7 +135,7 @@ append a string to call out your plugin if you wish). `from ttkHyperlinkLabel import HyperlinkLabel` and `import myNotebook as nb` - For creating UI elements. -In addition to the above we also explicitly package the following python +In addition to the above we also explicitly package the following python modules for plugin use: - shutil @@ -252,7 +252,7 @@ include variables, and even the returns of functions, in the output. ## Checking core EDMC version -If you have code that needs to act differently under different versions of +If you have code that needs to act differently under different versions of this application then you can check utilise `config.appversion`. Prior to version 5.0.0 this was a simple string. From 5.0.0 onwards it is, @@ -313,7 +313,7 @@ Mac, and `$TMP/EDMarketConnector.log` on Linux. ## Avoiding potential pitfalls -There are a number of things that your code should either do or avoiding +There are a number of things that your code should either do or avoiding doing so as to play nicely with the core EDMarketConnector code and not risk causing application crashes or hangs. @@ -324,12 +324,12 @@ See the section on [packaging extra modules](#your-plugin-directory-name-must-be ### Use a thread for long-running code -By default, your plugin code will be running in the main thread. So, if you -perform some operation that takes significant time (more than a second) you -will be blocking both the core code from continuing *and* any other plugins +By default, your plugin code will be running in the main thread. So, if you +perform some operation that takes significant time (more than a second) you +will be blocking both the core code from continuing *and* any other plugins from running their main-thread code. -This includes any connections to remote services, such as a website or +This includes any connections to remote services, such as a website or remote database. So please place such code within its own thread. See the [EDSM plugin](https://github.com/EDCD/EDMarketConnector/blob/main/plugins/edsm.py) @@ -338,20 +338,20 @@ with a queue to send data, and telling the sub-thread to stop during shutdown. ### All tkinter calls in main thread -The only tkinter calls that should ever be made from a sub-thread are +The only tkinter calls that should ever be made from a sub-thread are `event_generate()` calls to send data back to the main thread. -Any attempt to manipulate tkinter UI elements directly from a sub-thread +Any attempt to manipulate tkinter UI elements directly from a sub-thread will most likely crash the whole program. See the [EDSM plugin](https://github.com/EDCD/EDMarketConnector/blob/main/plugins/edsm.py) -code for an example of using `event_generate()` to cause the plugin main -thread code to update a UI element. Start from the `plugin_app()` +code for an example of using `event_generate()` to cause the plugin main +thread code to update a UI element. Start from the `plugin_app()` implementation. ### Do not call tkinter `event_generate` during shutdown. -However, you must **not** make *any* tkinter `event_generate()` call whilst +However, you must **not** make *any* tkinter `event_generate()` call whilst the application is shutting down. The application shutdown sequence is itself triggered from the `<>` event @@ -359,8 +359,8 @@ handler, and generating another event from any code in, or called from, there causes the application to hang somewhere in the tk libraries. You can detect if the application is shutting down with the boolean -`config.shutting_down`. Note that although this is technically a function -its implementation is of a property on `config.AbstractConfig` and thus you +`config.shutting_down`. Note that although this is technically a function +its implementation is of a property on `config.AbstractConfig` and thus you should treat it as a variable. **Do NOT use**: @@ -372,7 +372,7 @@ should treat it as a variable. # During shutdown ``` -as this will cause the 'During shutdown' branch to *always* be taken, as in +as this will cause the 'During shutdown' branch to *always* be taken, as in this context you're testing if the function exists, and that is always True. So instead use: @@ -417,8 +417,8 @@ your plugin's settings in a platform-independent way. Previously this was done with a single set and two get methods, the new methods provide better type safety. -If you want to maintain compatibility with pre-5.0.0 versions of this -application (please encourage plugin users to update!) then you'll need to +If you want to maintain compatibility with pre-5.0.0 versions of this +application (please encourage plugin users to update!) then you'll need to include this code in at least once in your plugin (no harm in putting it in all modules/files): @@ -655,6 +655,10 @@ Content of `state` (updated to the current journal entry): | `ShipType` | `str` | Internal name for the current ship type | | `HullValue` | `int` | Current ship value, excluding modules | | `ModulesValue` | `int` | Value of the current ship's modules | +| `UnladenMass` | `float` | Unladen mass of current ship | +| `CargoCapacity` | `int` | Max cargo capacity of current ship | +| `MaxJumpRange` | `float` | Unladen jump range of current ship | +| `FuelCapacity` | `dict[str,float]` | Current max capacity of Main & Reserve tanks | | `Rebuy` | `int` | Current ship's rebuy cost | | `Modules` | `dict` | Currently fitted modules | | `NavRoute` | `dict` | Last plotted multi-hop route[1] | @@ -684,6 +688,7 @@ Content of `state` (updated to the current journal entry): | `StationName`[3] | `Optional[str]` | Name of the station we're docked at, if applicable | | `MarketID`[3] | `Optional[str]` | MarketID of the station we're docked at, if applicable | | `StationType`[3] | `Optional[str]` | Type of the station we're docked at, if applicable | +| `Powerplay` | `dict` | `dict` of information on Powerplay [1] - Contents of `NavRoute` not changed if a `NavRouteClear` event is seen, but plugins will see the `NavRouteClear` event. @@ -699,8 +704,8 @@ cause `state['NavRoute'] = None`, but if you open the galaxy map in-game and cause an automatic re-plot of last route, then a new `NavRoute` event will also be generated and passed to plugins. -[2] - Some data from the CAPI is sometimes returned as a `list` (when all -members are present) and other times as an integer-keyed `dict` (when at +[2] - Some data from the CAPI is sometimes returned as a `list` (when all +members are present) and other times as an integer-keyed `dict` (when at least one member is missing, so the indices are not contiguous). We choose to always convert to the integer-keyed `dict` form so that code utilising the data is simpler. @@ -751,7 +756,7 @@ Journal `ModuleInfo` event. `OnFoot` is an indication as to if the player is on-foot, rather than in a vehicle. -`Component`, `Item`, `Consumable` & `Data` are `dict`s tracking your +`Component`, `Item`, `Consumable` & `Data` are `dict`s tracking your Odyssey MicroResources in your Ship Locker. `BacKPack` contains `dict`s for the same when you're on-foot. @@ -760,10 +765,10 @@ relating to suits and their loadouts. New in version 5.0.1: -`Odyssey` boolean based on the presence of such a flag in the `LoadGame` +`Odyssey` boolean based on the presence of such a flag in the `LoadGame` event. Defaults to `False`, i.e. if no such key in the event. -The previously undocumented `Horizons` boolean is similarly from `LoadGame`, +The previously undocumented `Horizons` boolean is similarly from `LoadGame`, but blindly retrieves the value rather than having a strict default. There'd be an exception if it wasn't there, and the value would be `None`. Note that this is **NOT** the same as the return from @@ -821,7 +826,7 @@ if that's what was in the file. New in version 5.8.0: -`StarPos`, `SystemAddress`, `SystemName` and `SystemPopulation` have been +`StarPos`, `SystemAddress`, `SystemName` and `SystemPopulation` have been added to the `state` dictionary. Best efforts data pertaining to the star system the player is in. @@ -832,6 +837,12 @@ the 'Body' name value. `StationName`, `MarketID`, and `StationType` added to the `state` dictionary. +New in version 5.13.0: + +`state` now has `Powerplay`, a `dict` including `Rank`, `Merits`, `Power`, +`TimePledged`, and `Votes`. `Votes` should only be populated if playing in +legacy mode, as it is no longer a concept in the current version of the game. + ___ ##### Synthetic Events @@ -853,8 +864,8 @@ react to either in your plugin code then either compare in a case insensitive manner or check for both. The difference in case allows you to differentiate between the two scenarios. -**NB: Any of these events are passing to `journal_entry_cqc` rather than to -`journal_entry` if player has loaded into Arena (CQC).** +**NB: Any of these events are passing to `journal_entry_cqc` rather than to +`journal_entry` if player has loaded into Arena (CQC).** This event is not sent when EDMarketConnector is running on a different machine so you should not *rely* on receiving this event. @@ -871,15 +882,15 @@ Examples of this are: 1. Every `NavRoute` event contains the full `Route` array as loaded from `NavRoute.json`. - + *NB: There is no indication available when a player cancels a route.* The game itself does not provide any such, not in a Journal event, not in a `Status.json` flag. - + The Journal documentation v28 is incorrect about the event and file being `Route(.json)` the word is `NavRoute`. Also the format of the data is, e.g. - + ```json { "timestamp":"2021-03-10T11:31:37Z", "event":"NavRoute", @@ -893,9 +904,9 @@ Examples of this are: ``` 1. Every `ModuleInfo` event contains the full data as loaded from the - `ModulesInfo.json` file. Note that we use the singular form here to + `ModulesInfo.json` file. Note that we use the singular form here to stay consistent with the Journal event name. - + --- ### Journal entry in CQC @@ -955,7 +966,7 @@ def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]): sys.stderr.write("Hardpoints {}\n".format(is_deployed and "deployed" or "stowed")) ``` -`dashboard_entry()` is called with the latest data from the `Status.json` +`dashboard_entry()` is called with the latest data from the `Status.json` file when an update to that file is detected. This will be when something on the player's cockpit display changes - @@ -1193,19 +1204,34 @@ widget if you need to display routine status information. ## Localisation You can localise your plugin to one of the languages that EDMarketConnector -itself supports. Add the following boilerplate near the top of each source +itself supports. Add the following boilerplate near the top of the source file that contains strings that needs translating: ```python import l10n import functools -_ = functools.partial(l10n.Translations.translate, context=__file__) +plugin_tl = functools.partial(l10n.translations.tl, context=__file__) + ``` -Wrap each string that needs translating with the `_()` function, e.g.: +Wrap each string that needs translating with the `plugin_tl()` function, e.g.: ```python - somewidget["text"] = _("Happy!") + somewidget["text"] = plugin_tl("Happy!") +``` + +Note that you can name the "plugin_tl" function whatever you want - just make sure to stay consistent! +Many plugins use `_` as the singleton name. We discourage that in versions 5.11 onward, but it should still work. +If your plugin has multiple files that need translations, simply import the `plugin_tl` function to that location. +You should only need to add the boilerplate once. + +If you wish to override EDMCs current language when translating, +`l10n.translations.tl()` also takes an optional `lang` parameter which can +be passed a language identifier. For example to define a function to override +all translations to German: + +```python +plugin_tl_de = functools.partial(l10n.Translations.translate, context=__file__, lang="de") ``` If you display localized strings in EDMarketConnector's main window you should @@ -1262,11 +1288,11 @@ Any modules the core application code uses will naturally be packaged, and we explicitly include a small number of additional modules for the use of plugins. -Whilst we would like to make all of the `stdlib` of Python available it is -not automatically packaged into our releases by py2exe. We hope to address -this in the 5.3 release series. In the meantime, if there's anything -missing that you'd like to use, please ask. Yes, this very much means you -need to test your plugins against a Windows installation of the application +Whilst we would like to make all of the `stdlib` of Python available it is +not automatically packaged into our releases by py2exe. We hope to address +this in the 5.3 release series. In the meantime, if there's anything +missing that you'd like to use, please ask. Yes, this very much means you +need to test your plugins against a Windows installation of the application to be sure it will work. See @@ -1416,7 +1442,7 @@ versions of EDMarketConnector: [2to3](https://docs.python.org/3/library/2to3.html) tool can automate much of this work. -We advise *against* making any attempt to have a plugin's code work under -both Python 2.7 and 3.x. We no longer maintain the Python 2.7-based -versions of this application, and you shouldn't support use of them with +We advise *against* making any attempt to have a plugin's code work under +both Python 2.7 and 3.x. We no longer maintain the Python 2.7-based +versions of this application, and you shouldn't support use of them with your plugin. diff --git a/README.md b/README.md index 1d297395..600df075 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,6 @@ Please see the [Acknowledgements](https://github.com/EDCD/EDMarketConnector/wiki License ------- -Copyright Š 2015-2019 Jonathan Harris, 2020-2021 EDCD +Copyright Š 2015-2019 Jonathan Harris, 2020-2024 EDCD Licensed under the [GNU Public License (GPL)](http://www.gnu.org/licenses/gpl-2.0.html) version 2 or later. diff --git a/build.py b/build.py index e1d37f49..d14b4f62 100644 --- a/build.py +++ b/build.py @@ -10,7 +10,6 @@ import shutil import sys import pathlib from string import Template -from os.path import join, isdir import py2exe from config import ( appcmdname, @@ -21,6 +20,7 @@ from config import ( _static_appversion, update_interval ) +from update import check_for_fdev_updates def iss_build(template_path: str, output_file: str) -> None: @@ -36,7 +36,7 @@ def iss_build(template_path: str, output_file: str) -> None: new_file.write(newfile) -def system_check(dist_dir: str) -> str: +def system_check(dist_dir: pathlib.Path) -> str: """Check if the system is able to build.""" if sys.version_info < (3, 11): sys.exit(f"Unexpected Python version {sys.version}") @@ -54,17 +54,18 @@ def system_check(dist_dir: str) -> str: print(f"Git short hash: {git_shorthash}") - if dist_dir and len(dist_dir) > 1 and isdir(dist_dir): + if dist_dir and pathlib.Path.is_dir(dist_dir): shutil.rmtree(dist_dir) return gitversion_file def generate_data_files( app_name: str, gitversion_file: str, plugins: list[str] -) -> list[tuple[str, list[str]]]: +) -> list[tuple[object, object]]: """Create the required datafiles to build.""" l10n_dir = "L10n" - fdevids_dir = "FDevIDs" + fdevids_dir = pathlib.Path("FDevIDs") + license_dir = pathlib.Path("docs/Licenses") data_files = [ ( "", @@ -76,12 +77,10 @@ def generate_data_files( "ChangeLog.md", "snd_good.wav", "snd_bad.wav", - "modules.p", # TODO: Remove in 6.0 - "resources/modules.json", - "resources/ships.json", - "ships.p", # TODO: Remove in 6.0 - f"{app_name}.VisualElementsManifest.xml", + "modules.json", + "ships.json", f"{app_name}.ico", + f"resources/{appcmdname}.ico", "EDMarketConnector - TRACE.bat", "EDMarketConnector - localserver-auth.bat", "EDMarketConnector - reset-ui.bat", @@ -89,23 +88,29 @@ def generate_data_files( ), ( l10n_dir, - [join(l10n_dir, x) for x in os.listdir(l10n_dir) if x.endswith(".strings")], + [pathlib.Path(l10n_dir) / x for x in os.listdir(l10n_dir) if x.endswith(".strings")] ), ( fdevids_dir, [ - join(fdevids_dir, "commodity.csv"), - join(fdevids_dir, "rare_commodity.csv"), + pathlib.Path(fdevids_dir / "commodity.csv"), + pathlib.Path(fdevids_dir / "rare_commodity.csv"), ], ), ("plugins", plugins), ] + # Add all files recursively from license directories + for root, dirs, files in os.walk(license_dir): + file_list = [os.path.join(root, f) for f in files] + dest_dir = os.path.join(license_dir, os.path.relpath(root, license_dir)) + data_files.append((dest_dir, file_list)) + return data_files def build() -> None: """Build EDMarketConnector using Py2Exe.""" - dist_dir: str = "dist.win32" + dist_dir: pathlib.Path = pathlib.Path("dist.win32") gitversion_filename: str = system_check(dist_dir) # Constants @@ -124,7 +129,6 @@ def build() -> None: "packages": [ "asyncio", "multiprocessing", - "pkg_resources._vendor.platformdirs", "sqlite3", "util", ], @@ -133,7 +137,6 @@ def build() -> None: "distutils", "_markerlib", "optparse", - "PIL", "simplejson", "unittest", "doctest", @@ -144,7 +147,7 @@ def build() -> None: } # Function to generate DATA_FILES list - data_files: list[tuple[str, list[str]]] = generate_data_files( + data_files: list[tuple[object, object]] = generate_data_files( appname, gitversion_filename, plugins ) @@ -165,27 +168,38 @@ def build() -> None: "script": "EDMarketConnector.py", "icon_resources": [(0, f"{appname}.ico")], "other_resources": [ - (24, 1, pathlib.Path(f"{appname}.manifest").read_text(encoding="UTF8")) + (24, 1, pathlib.Path(f"resources/{appname}.manifest").read_text(encoding="UTF8")) ], } console_config: dict = { "dest_base": appcmdname, "script": "EDMC.py", + "icon_resources": [(0, f"resources/{appcmdname}.ico")], "other_resources": [ - (24, 1, pathlib.Path(f"{appcmdname}.manifest").read_text(encoding="UTF8")) + (24, 1, pathlib.Path(f"resources/{appcmdname}.manifest").read_text(encoding="UTF8")) + ], + } + + checker_config: dict = { + "dest_base": "EDMCSystemProfiler", + "script": "EDMCSystemProfiler.py", + "icon_resources": [(0, f"{appname}.ico")], + "other_resources": [ + (24, 1, pathlib.Path(f"resources/{appname}.manifest").read_text(encoding="UTF8")) ], } try: py2exe.freeze( version_info=version_info, - windows=[windows_config], + windows=[windows_config, checker_config], console=[console_config], data_files=data_files, options=options, ) - except FileNotFoundError: + except FileNotFoundError as err: + print(err) sys.exit( "Build Failed due to Missing Files! Have you set up your submodules? \n" "https://github.com/EDCD/EDMarketConnector/wiki/Running-from-source" @@ -199,4 +213,5 @@ def build() -> None: if __name__ == "__main__": + check_for_fdev_updates(local=True) build() diff --git a/collate.py b/collate.py index caf5994f..bba94df1 100755 --- a/collate.py +++ b/collate.py @@ -17,11 +17,11 @@ import json import os import pathlib import sys -from os.path import isfile from traceback import print_exc import companion import outfitting +from config import config from edmc_data import companion_category_map, ship_name_map @@ -34,7 +34,7 @@ def __make_backup(file_name: pathlib.Path, suffix: str = '.bak') -> None: """ backup_name = file_name.parent / (file_name.name + suffix) - if isfile(backup_name): + if pathlib.Path.is_file(backup_name): os.unlink(backup_name) os.rename(file_name, backup_name) @@ -50,11 +50,14 @@ def addcommodities(data) -> None: # noqa: CCR001 if not data['lastStarport'].get('commodities'): return - commodityfile = pathlib.Path('FDevIDs/commodity.csv') + try: + commodityfile = config.app_dir_path / 'FDevIDs' / 'commodity.csv' + except FileNotFoundError: + commodityfile = pathlib.Path('FDevIDs/commodity.csv') commodities = {} # slurp existing - if isfile(commodityfile): + if pathlib.Path.is_file(commodityfile): with open(commodityfile) as csvfile: reader = csv.DictReader(csvfile) for row in reader: @@ -82,7 +85,7 @@ def addcommodities(data) -> None: # noqa: CCR001 if len(commodities) <= size_pre: return - if isfile(commodityfile): + if pathlib.Path.is_file(commodityfile): __make_backup(commodityfile) with open(commodityfile, 'w', newline='\n') as csvfile: @@ -105,7 +108,7 @@ def addmodules(data): # noqa: C901, CCR001 fields = ('id', 'symbol', 'category', 'name', 'mount', 'guidance', 'ship', 'class', 'rating', 'entitlement') # slurp existing - if isfile(outfile): + if pathlib.Path.is_file(outfile): with open(outfile) as csvfile: reader = csv.DictReader(csvfile, restval='') for row in reader: @@ -143,7 +146,7 @@ def addmodules(data): # noqa: C901, CCR001 if not len(modules) > size_pre: return - if isfile(outfile): + if pathlib.Path.is_file(outfile): __make_backup(outfile) with open(outfile, 'w', newline='\n') as csvfile: @@ -166,7 +169,7 @@ def addships(data) -> None: # noqa: CCR001 fields = ('id', 'symbol', 'name') # slurp existing - if isfile(shipfile): + if pathlib.Path.is_file(shipfile): with open(shipfile) as csvfile: reader = csv.DictReader(csvfile, restval='') for row in reader: @@ -196,7 +199,7 @@ def addships(data) -> None: # noqa: CCR001 if not len(ships) > size_pre: return - if isfile(shipfile): + if pathlib.Path.is_file(shipfile): __make_backup(shipfile) with open(shipfile, 'w', newline='\n') as csvfile: diff --git a/commodity.py b/commodity.py index 6b167fa1..52c0f8b8 100644 --- a/commodity.py +++ b/commodity.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import time -from os.path import join +from pathlib import Path from config import config from edmc_data import commodity_bracketmap as bracketmap @@ -29,7 +29,7 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None: filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)) filename_kind = 'csv' filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}' - filename = join(config.get_str('outdir'), filename) + filename = Path(config.get_str('outdir')) / filename if kind == COMMODITY_CSV: sep = ';' # BUG: for fixing later after cleanup diff --git a/companion.py b/companion.py index 217370d5..9d655bc6 100644 --- a/companion.py +++ b/companion.py @@ -27,8 +27,9 @@ import tkinter as tk import urllib.parse import webbrowser from email.utils import parsedate +from pathlib import Path from queue import Queue -from typing import TYPE_CHECKING, Any, Mapping, OrderedDict, TypeVar +from typing import TYPE_CHECKING, Any, Mapping, TypeVar import requests import config as conf_module import killswitch @@ -37,12 +38,11 @@ from config import config, user_agent from edmc_data import companion_category_map as category_map from EDMCLogging import get_main_logger from monitor import monitor +from l10n import translations as tr logger = get_main_logger() if TYPE_CHECKING: - def _(x): return x - UserDict = collections.UserDict[str, Any] # indicate to our type checkers what this generic class holds normally else: UserDict = collections.UserDict # Otherwise simply use the actual class @@ -224,7 +224,7 @@ class ServerError(Exception): self.args = args if not args: # LANG: Frontier CAPI didn't respond - self.args = (_("Error: Frontier CAPI didn't respond"),) + self.args = (tr.tl("Error: Frontier CAPI didn't respond"),) class ServerConnectionError(ServerError): @@ -243,7 +243,7 @@ class ServerLagging(Exception): self.args = args if not args: # LANG: Frontier CAPI data doesn't agree with latest Journal game location - self.args = (_('Error: Frontier server is lagging'),) + self.args = (tr.tl('Error: Frontier server is lagging'),) class NoMonitorStation(Exception): @@ -259,7 +259,7 @@ class NoMonitorStation(Exception): self.args = args if not args: # LANG: Commander is docked at an EDO settlement, got out and back in, we forgot the station - self.args = (_("Docked but unknown station: EDO Settlement?"),) + self.args = (tr.tl("Docked but unknown station: EDO Settlement?"),) class CredentialsError(Exception): @@ -269,7 +269,7 @@ class CredentialsError(Exception): self.args = args if not args: # LANG: Generic "something went wrong with Frontier Auth" error - self.args = (_('Error: Invalid Credentials'),) + self.args = (tr.tl('Error: Invalid Credentials'),) class CredentialsRequireRefresh(Exception): @@ -294,7 +294,7 @@ class CmdrError(Exception): self.args = args if not args: # LANG: Frontier CAPI authorisation not for currently game-active commander - self.args = (_('Error: Wrong Cmdr'),) + self.args = (tr.tl('Error: Wrong Cmdr'),) class Auth: @@ -429,7 +429,7 @@ class Auth: '' ) # LANG: Generic error prefix - following text is from Frontier auth service - raise CredentialsError(f'{_("Error")}: {error!r}') + raise CredentialsError(f'{tr.tl("Error")}: {error!r}') r = None try: @@ -472,18 +472,18 @@ class Auth: if (usr := data_decode.get('usr')) is None: logger.error('No "usr" in /decode data') # LANG: Frontier auth, no 'usr' section in returned data - raise CredentialsError(_("Error: Couldn't check token customer_id")) + raise CredentialsError(tr.tl("Error: Couldn't check token customer_id")) if (customer_id := usr.get('customer_id')) is None: logger.error('No "usr"->"customer_id" in /decode data') # LANG: Frontier auth, no 'customer_id' in 'usr' section in returned data - raise CredentialsError(_("Error: Couldn't check token customer_id")) + raise CredentialsError(tr.tl("Error: Couldn't check token customer_id")) # All 'FID' seen in Journals so far have been 'F' # Frontier, Steam and Epic if f'F{customer_id}' != monitor.state.get('FID'): # LANG: Frontier auth customer_id doesn't match game session FID - raise CredentialsError(_("Error: customer_id doesn't match!")) + raise CredentialsError(tr.tl("Error: customer_id doesn't match!")) logger.info(f'Frontier CAPI Auth: New token for \"{self.cmdr}\"') cmdrs = config.get_list('cmdrs', default=[]) @@ -505,7 +505,7 @@ class Auth: self.dump(r) # LANG: Failed to get Access Token from Frontier Auth service - raise CredentialsError(_('Error: unable to get token')) from e + raise CredentialsError(tr.tl('Error: unable to get token')) from e logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") self.dump(r) @@ -514,7 +514,7 @@ class Auth: '' ) # LANG: Generic error prefix - following text is from Frontier auth service - raise CredentialsError(f'{_("Error")}: {error!r}') + raise CredentialsError(f'{tr.tl("Error")}: {error!r}') @staticmethod def invalidate(cmdr: str | None) -> None: @@ -841,7 +841,7 @@ class Session: except Exception as e: logger.debug('Attempting GET', exc_info=e) # LANG: Frontier CAPI data retrieval failed - raise ServerError(f'{_("Frontier CAPI query failure")}: {capi_endpoint}') from e + raise ServerError(f'{tr.tl("Frontier CAPI query failure")}: {capi_endpoint}') from e if capi_endpoint == self.FRONTIER_CAPI_PATH_PROFILE and 'commander' not in capi_data: logger.error('No commander in returned data') @@ -874,7 +874,7 @@ class Session: if response.status_code == 418: # "I'm a teapot" - used to signal maintenance # LANG: Frontier CAPI returned 418, meaning down for maintenance - raise ServerError(_("Frontier CAPI down for maintenance")) + raise ServerError(tr.tl("Frontier CAPI down for maintenance")) logger.exception('Frontier CAPI: Misc. Error') raise ServerError('Frontier CAPI: Misc. Error') @@ -1064,7 +1064,7 @@ class Session: play_sound: bool = False, auto_update: bool = False ) -> None: """ - Perform CAPI query for fleetcarrier data. + Perform CAPI query for Fleet Carrier data. :param query_time: When this query was initiated. :param tk_response_event: Name of tk event to generate when response queued. @@ -1075,8 +1075,8 @@ class Session: if not capi_host: return - # Ask the thread worker to perform a fleetcarrier query - logger.trace_if('capi.worker', 'Enqueueing fleetcarrier request') + # Ask the thread worker to perform a Fleet Carrier query + logger.trace_if('capi.worker', 'Enqueueing Fleet Carrier request') self.capi_request_queue.put( EDMCCAPIRequest( capi_host=capi_host, @@ -1136,7 +1136,7 @@ class Session: def dump_capi_data(self, data: CAPIData) -> None: """Dump CAPI data to file for examination.""" - if os.path.isdir('dump'): + if Path('dump').is_dir(): file_name: str = "" if data.source_endpoint == self.FRONTIER_CAPI_PATH_FLEETCARRIER: file_name += f"FleetCarrier.{data['name']['callsign']}" @@ -1204,10 +1204,10 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully if not commodity_map: # Lazily populate for f in ('commodity.csv', 'rare_commodity.csv'): - if not os.path.isfile(config.respath_path / 'FDevIDs/' / f): + if not (config.app_dir_path / 'FDevIDs' / f).is_file(): logger.warning(f'FDevID file {f} not found! Generating output without these commodity name rewrites.') continue - with open(config.respath_path / 'FDevIDs' / f, 'r') as csvfile: + with open(config.app_dir_path / 'FDevIDs' / f, 'r') as csvfile: reader = csv.DictReader(csvfile) for row in reader: @@ -1328,7 +1328,7 @@ def index_possibly_sparse_list(data: Mapping[str, V] | list[V], key: int) -> V: if isinstance(data, list): return data[key] - if isinstance(data, (dict, OrderedDict)): + if isinstance(data, (dict, dict)): return data[str(key)] raise ValueError(f'Unexpected data type {type(data)}') diff --git a/config/__init__.py b/config/__init__.py index 77b6a0f7..adf3e47f 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -7,7 +7,6 @@ See LICENSE file. Windows uses the Registry to store values in a flat manner. Linux uses a file, but for commonality it's still a flat data structure. -macOS uses a 'defaults' object. """ from __future__ import annotations @@ -18,7 +17,6 @@ __all__ = [ 'applongname', 'appcmdname', 'copyright', - 'update_feed', 'update_interval', 'debug_senders', 'trace_on', @@ -30,7 +28,8 @@ __all__ = [ 'user_agent', 'appversion_nobuild', 'AbstractConfig', - 'config' + 'config', + 'get_update_feed', ] import abc @@ -41,7 +40,6 @@ import pathlib import re import subprocess import sys -import traceback import warnings from abc import abstractmethod from typing import Any, Callable, Type, TypeVar @@ -54,12 +52,11 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.10.0' - +_static_appversion = '5.12.2' _cached_version: semantic_version.Version | None = None -copyright = 'Š 2015-2019 Jonathan Harris, 2020-2023 EDCD' +copyright = 'Š 2015-2019 Jonathan Harris, 2020-2024 EDCD' + -update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' update_interval = 8*60*60 # 8 Hours # Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file debug_senders: list[str] = [] @@ -81,15 +78,15 @@ else: _T = TypeVar('_T') -def git_shorthash_from_head() -> str: +def git_shorthash_from_head() -> str | None: """ Determine short hash for current git HEAD. Includes `.DIRTY` if any changes have been made from HEAD - :return: str - None if we couldn't determine the short hash. + :return: str | None: None if we couldn't determine the short hash. """ - shorthash: str = None # type: ignore + shorthash: str | None = None try: git_cmd = subprocess.Popen( @@ -99,14 +96,14 @@ def git_shorthash_from_head() -> str: ) out, err = git_cmd.communicate() - except subprocess.CalledProcessError as e: + except (subprocess.CalledProcessError, FileNotFoundError) as e: logger.info(f"Couldn't run git command for short hash: {e!r}") else: shorthash = out.decode().rstrip('\n') if re.match(r'^[0-9a-f]{7,}$', shorthash) is None: logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None") - shorthash = None # type: ignore + shorthash = None if shorthash is not None: with contextlib.suppress(Exception): @@ -134,13 +131,19 @@ def appversion() -> semantic_version.Version: # Running frozen, so we should have a .gitversion file # Yes, .parent because if frozen we're inside library.zip with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, encoding='utf-8') as gitv: - shorthash = gitv.read() + shorthash: str | None = gitv.read() else: - # Running from source + # Running from source. Use git rev-parse --short HEAD + # or fall back to .gitversion file if it exists. + # This is also required for the Flatpak shorthash = git_shorthash_from_head() if shorthash is None: - shorthash = 'UNKNOWN' + if pathlib.Path(sys.path[0] + "/" + GITVERSION_FILE).exists(): + with open(pathlib.Path(sys.path[0] + "/" + GITVERSION_FILE), encoding='utf-8') as gitv: + shorthash = gitv.read() + else: + shorthash = 'UNKNOWN' _cached_version = semantic_version.Version(f'{_static_appversion}+{shorthash}') return _cached_version @@ -186,6 +189,7 @@ class AbstractConfig(abc.ABC): app_dir_path: pathlib.Path plugin_dir_path: pathlib.Path + default_plugin_dir_path: pathlib.Path internal_plugin_dir_path: pathlib.Path respath_path: pathlib.Path home_path: pathlib.Path @@ -276,6 +280,11 @@ class AbstractConfig(abc.ABC): """Return a string version of plugin_dir.""" return str(self.plugin_dir_path) + @property + def default_plugin_dir(self) -> str: + """Return a string version of plugin_dir.""" + return str(self.default_plugin_dir_path) + @property def internal_plugin_dir(self) -> str: """Return a string version of internal_plugin_dir.""" @@ -324,8 +333,8 @@ class AbstractConfig(abc.ABC): :raises OSError: On Windows, if a Registry error occurs. :return: The data or the default. """ - warnings.warn(DeprecationWarning('get is Deprecated. use the specific getter for your type')) - logger.debug('Attempt to use Deprecated get() method\n' + ''.join(traceback.format_stack())) + # DEPRECATED: Migrate to specific type getters. Will remove in 6.0 or later. + warnings.warn('get is Deprecated. use the specific getter for your type', DeprecationWarning, stacklevel=2) if (a_list := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None: return a_list @@ -383,8 +392,8 @@ class AbstractConfig(abc.ABC): See get_int for its replacement. :raises OSError: On Windows, if a Registry error occurs. """ - warnings.warn(DeprecationWarning('getint is Deprecated. Use get_int instead')) - logger.debug('Attempt to use Deprecated getint() method\n' + ''.join(traceback.format_stack())) + # DEPRECATED: Migrate to get_int. Will remove in 6.0 or later. + warnings.warn('getint is Deprecated. Use get_int instead', DeprecationWarning, stacklevel=2) return self.get_int(key, default=default) @@ -441,17 +450,19 @@ class AbstractConfig(abc.ABC): """Close this config and release any associated resources.""" raise NotImplementedError +# DEPRECATED: Password system doesn't do anything. Will remove in 6.0 or later. def get_password(self, account: str) -> None: """Legacy password retrieval.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) def set_password(self, account: str, password: str) -> None: """Legacy password setting.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) def delete_password(self, account: str) -> None: """Legacy password deletion.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) +# End Dep Zone def get_config(*args, **kwargs) -> AbstractConfig: @@ -462,10 +473,6 @@ def get_config(*args, **kwargs) -> AbstractConfig: :param kwargs: Args to be passed through to implementation. :return: Instance of the implementation. """ - if sys.platform == "darwin": # pragma: sys-platform-darwin - from .darwin import MacConfig - return MacConfig(*args, **kwargs) - if sys.platform == "win32": # pragma: sys-platform-win32 from .windows import WinConfig return WinConfig(*args, **kwargs) @@ -478,3 +485,20 @@ def get_config(*args, **kwargs) -> AbstractConfig: config = get_config() + + +# Wiki: https://github.com/EDCD/EDMarketConnector/wiki/Participating-in-Open-Betas-of-EDMC +def get_update_feed() -> str: + """Select the proper update feed for the current update track.""" + if config.get_bool('beta_optin'): + return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector-beta.xml' + return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' + + +# DEPRECATED: Migrate to get_update_feed(). Will remove in 6.0 or later. +def __getattr__(name: str): + if name == 'update_feed': + warnings.warn('update_feed is deprecated, and will be removed in 6.0 or later. ' + 'Please migrate to get_update_feed()', DeprecationWarning, stacklevel=2) + return get_update_feed() + raise AttributeError(name=name) diff --git a/config/darwin.py b/config/darwin.py deleted file mode 100644 index 9c15ec32..00000000 --- a/config/darwin.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -darwin.py - Darwin/macOS implementation of AbstractConfig. - -Copyright (c) EDCD, All Rights Reserved -Licensed under the GNU General Public License. -See LICENSE file. -""" -from __future__ import annotations - -import pathlib -import sys -from typing import Any -from Foundation import ( # type: ignore - NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults, - NSUserDomainMask -) -from config import AbstractConfig, appname, logger - -assert sys.platform == 'darwin' - - -class MacConfig(AbstractConfig): - """MacConfig is the implementation of AbstractConfig for Darwin based OSes.""" - - def __init__(self) -> None: - super().__init__() - support_path = pathlib.Path( - NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSUserDomainMask, True - )[0] - ) - - self.app_dir_path = support_path / appname - self.app_dir_path.mkdir(exist_ok=True) - - self.plugin_dir_path = self.app_dir_path / 'plugins' - self.plugin_dir_path.mkdir(exist_ok=True) - - # Bundle IDs identify a singled app though out a system - - if getattr(sys, 'frozen', False): - exe_dir = pathlib.Path(sys.executable).parent - self.internal_plugin_dir_path = exe_dir.parent / 'Library' / 'plugins' - self.respath_path = exe_dir.parent / 'Resources' - self.identifier = NSBundle.mainBundle().bundleIdentifier() - - else: - file_dir = pathlib.Path(__file__).parent.parent - self.internal_plugin_dir_path = file_dir / 'plugins' - self.respath_path = file_dir - - self.identifier = f'uk.org.marginal.{appname.lower()}' - NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier - - self.default_journal_dir_path = support_path / 'Frontier Developments' / 'Elite Dangerous' - self._defaults: Any = NSUserDefaults.standardUserDefaults() - self._settings: dict[str, int | str | list] = dict( - self._defaults.persistentDomainForName_(self.identifier) or {} - ) # make writeable - - if (out_dir := self.get_str('out_dir')) is None or not pathlib.Path(out_dir).exists(): - self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0]) - - def __raw_get(self, key: str) -> None | list | str | int: - """ - Retrieve the raw data for the given key. - - :param str: str - The key data is being requested for. - :return: The requested data. - """ - res = self._settings.get(key) - # On MacOS Catalina, with python.org python 3.9.2 any 'list' - # has type __NSCFArray so a simple `isinstance(res, list)` is - # False. So, check it's not-None, and not the other types. - # - # If we can find where to import the definition of NSCFArray - # then we could possibly test against that. - if res is not None and not isinstance(res, str) and not isinstance(res, int): - return list(res) - - return res - - def get_str(self, key: str, *, default: str = None) -> str: - """ - Return the string referred to by the given key if it exists, or the default. - - Implements :meth:`AbstractConfig.get_str`. - """ - res = self.__raw_get(key) - if res is None: - return default # Yes it could be None, but we're _assuming_ that people gave us a default - - if not isinstance(res, str): - raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}') - - return res - - def get_list(self, key: str, *, default: list = None) -> list: - """ - Return the list referred to by the given key if it exists, or the default. - - Implements :meth:`AbstractConfig.get_list`. - """ - res = self.__raw_get(key) - if res is None: - return default # Yes it could be None, but we're _assuming_ that people gave us a default - - if not isinstance(res, list): - raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') - - return res - - def get_int(self, key: str, *, default: int = 0) -> int: - """ - Return the int referred to by key if it exists in the config. - - Implements :meth:`AbstractConfig.get_int`. - """ - res = self.__raw_get(key) - if res is None: - return default - - if not isinstance(res, (str, int)): - raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') - - try: - return int(res) - - except ValueError as e: - logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {e}') - return default # Yes it could be None, but we're _assuming_ that people gave us a default - - def get_bool(self, key: str, *, default: bool = None) -> bool: - """ - Return the bool referred to by the given key if it exists, or the default. - - Implements :meth:`AbstractConfig.get_bool`. - """ - res = self.__raw_get(key) - if res is None: - return default # Yes it could be None, but we're _assuming_ that people gave us a default - - if not isinstance(res, bool): - raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') - - return res - - def set(self, key: str, val: int | str | list[str] | bool) -> None: - """ - Set the given key's data to the given value. - - Implements :meth:`AbstractConfig.set`. - """ - if self._settings is None: - raise ValueError('attempt to use a closed _settings') - - if not isinstance(val, (bool, str, int, list)): - raise ValueError(f'Unexpected type for value {type(val)=}') - - self._settings[key] = val - - def delete(self, key: str, *, suppress=False) -> None: - """ - Delete the given key from the config. - - Implements :meth:`AbstractConfig.delete`. - """ - try: - del self._settings[key] - - except Exception: - if suppress: - pass - - def save(self) -> None: - """ - Save the current configuration. - - Implements :meth:`AbstractConfig.save`. - """ - self._defaults.setPersistentDomain_forName_(self._settings, self.identifier) - self._defaults.synchronize() - - def close(self) -> None: - """ - Close this config and release any associated resources. - - Implements :meth:`AbstractConfig.close`. - """ - self.save() - self._defaults = None diff --git a/config/linux.py b/config/linux.py index 73100800..a7a472f4 100644 --- a/config/linux.py +++ b/config/linux.py @@ -5,6 +5,8 @@ Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. """ +from __future__ import annotations + import os import pathlib import sys @@ -29,9 +31,7 @@ class LinuxConfig(AbstractConfig): self.app_dir_path = xdg_data_home / appname self.app_dir_path.mkdir(exist_ok=True, parents=True) - self.plugin_dir_path = self.app_dir_path / 'plugins' - self.plugin_dir_path.mkdir(exist_ok=True) - + self.default_plugin_dir_path = self.app_dir_path / 'plugins' self.respath_path = pathlib.Path(__file__).parent.parent self.internal_plugin_dir_path = self.respath_path / 'plugins' @@ -60,6 +60,12 @@ class LinuxConfig(AbstractConfig): self.config.add_section(self.SECTION) + if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir(): + self.set("plugin_dir", str(self.default_plugin_dir_path)) + plugdir_str = self.default_plugin_dir + self.plugin_dir_path = pathlib.Path(plugdir_str) + self.plugin_dir_path.mkdir(exist_ok=True) + if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir(): self.set('outdir', self.home) diff --git a/config/windows.py b/config/windows.py index 75e3f697..550b0e4a 100644 --- a/config/windows.py +++ b/config/windows.py @@ -7,41 +7,23 @@ See LICENSE file. """ from __future__ import annotations -import ctypes import functools import pathlib import sys import uuid import winreg -from ctypes.wintypes import DWORD, HANDLE from typing import Literal from config import AbstractConfig, applongname, appname, logger +from win32comext.shell import shell assert sys.platform == 'win32' REG_RESERVED_ALWAYS_ZERO = 0 -# This is the only way to do this from python without external deps (which do this anyway). -FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') -FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}') -FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}') -FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}') - -SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath -SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)] - -CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree -CoTaskMemFree.argtypes = [ctypes.c_void_p] - def known_folder_path(guid: uuid.UUID) -> str | None: """Look up a Windows GUID to actual folder path name.""" - buf = ctypes.c_wchar_p() - if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)): - return None - retval = buf.value # copy data - CoTaskMemFree(buf) # and free original - return retval + return shell.SHGetKnownFolderPath(guid, 0, 0) class WinConfig(AbstractConfig): @@ -49,24 +31,6 @@ class WinConfig(AbstractConfig): def __init__(self) -> None: super().__init__() - self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore - self.app_dir_path.mkdir(exist_ok=True) - - self.plugin_dir_path = self.app_dir_path / 'plugins' - self.plugin_dir_path.mkdir(exist_ok=True) - - if getattr(sys, 'frozen', False): - self.respath_path = pathlib.Path(sys.executable).parent - self.internal_plugin_dir_path = self.respath_path / 'plugins' - else: - self.respath_path = pathlib.Path(__file__).parent.parent - self.internal_plugin_dir_path = self.respath_path / 'plugins' - - self.home_path = pathlib.Path.home() - - journal_dir_path = pathlib.Path( - known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore - self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806 create_key_defaults = functools.partial( @@ -82,9 +46,33 @@ class WinConfig(AbstractConfig): logger.exception('Could not create required registry keys') raise + if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData): + self.app_dir_path = pathlib.Path(local_appdata) / appname + self.app_dir_path.mkdir(exist_ok=True) + + self.default_plugin_dir_path = self.app_dir_path / 'plugins' + if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir(): + self.set("plugin_dir", str(self.default_plugin_dir_path)) + plugdir_str = self.default_plugin_dir + self.plugin_dir_path = pathlib.Path(plugdir_str) + self.plugin_dir_path.mkdir(exist_ok=True) + + if getattr(sys, 'frozen', False): + self.respath_path = pathlib.Path(sys.executable).parent + self.internal_plugin_dir_path = self.respath_path / 'plugins' + else: + self.respath_path = pathlib.Path(__file__).parent.parent + self.internal_plugin_dir_path = self.respath_path / 'plugins' + + self.home_path = pathlib.Path.home() + + journal_dir_path = pathlib.Path( + known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore + self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore + self.identifier = applongname if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir(): - docs = known_folder_path(FOLDERID_Documents) + docs = known_folder_path(shell.FOLDERID_Documents) self.set("outdir", docs if docs is not None else self.home) def __get_regentry(self, key: str) -> None | list | str | int: diff --git a/coriolis-data b/coriolis-data index 05b16a4c..c59e4b79 160000 --- a/coriolis-data +++ b/coriolis-data @@ -1 +1 @@ -Subproject commit 05b16a4c716980ea95a46d29205f7d3b1f957fb4 +Subproject commit c59e4b79dc48d581f9c5936d66a7f8bf533e8e6c diff --git a/coriolis-update-files.py b/coriolis-update-files.py index b68d9647..729d2ef6 100755 --- a/coriolis-update-files.py +++ b/coriolis-update-files.py @@ -16,7 +16,6 @@ FDevIDs/ version of the file, copy it over the local one. import json import subprocess import sys -from collections import OrderedDict import outfitting from edmc_data import coriolis_ship_map, ship_name_map @@ -52,12 +51,13 @@ if __name__ == "__main__": for m in list(data['Ships'].values()): name = coriolis_ship_map.get(m['properties']['name'], str(m['properties']['name'])) assert name in reverse_ship_map, name - ships[name] = {'hullMass': m['properties']['hullMass']} + ships[name] = {'hullMass': m['properties']['hullMass'], + 'reserveFuelCapacity': m['properties']['reserveFuelCapacity']} for i, bulkhead in enumerate(bulkheads): modules['_'.join([reverse_ship_map[name], 'armour', bulkhead])] = {'mass': m['bulkheads'][i]['mass']} - ships = OrderedDict([(k, ships[k]) for k in sorted(ships)]) # sort for easier diffing - with open("resources/ships.json", "w") as ships_file: + ships = {k: ships[k] for k in sorted(ships)} + with open("ships.json", "w") as ships_file: json.dump(ships, ships_file, indent=4) # Module masses @@ -87,10 +87,6 @@ if __name__ == "__main__": add(modules, 'int_stellarbodydiscoveryscanner_intermediate', {'mass': 2}) add(modules, 'int_stellarbodydiscoveryscanner_advanced', {'mass': 2}) - # Missing - add(modules, 'hpt_multicannon_fixed_small_advanced', {'mass': 2}) - add(modules, 'hpt_multicannon_fixed_medium_advanced', {'mass': 4}) - - modules = OrderedDict([(k, modules[k]) for k in sorted(modules)]) # sort for easier diffing - with open("resources/modules.json", "w") as modules_file: + modules = {k: modules[k] for k in sorted(modules)} + with open("modules.json", "w") as modules_file: json.dump(modules, modules_file, indent=4) diff --git a/dashboard.py b/dashboard.py index 1f95a943..cae2cc96 100644 --- a/dashboard.py +++ b/dashboard.py @@ -12,7 +12,7 @@ import sys import time import tkinter as tk from calendar import timegm -from os.path import getsize, isdir, isfile, join +from pathlib import Path from typing import Any, cast from watchdog.observers.api import BaseObserver from config import config @@ -20,13 +20,13 @@ from EDMCLogging import get_main_logger logger = get_main_logger() -if sys.platform in ('darwin', 'win32'): +if sys.platform == 'win32': from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer else: # Linux's inotify doesn't work over CIFS or NFS, so poll class FileSystemEventHandler: # type: ignore - """Dummy class to represent a file system event handler on platforms other than macOS and Windows.""" + """Dummy class to represent a file system event handler on platforms other than Windows.""" class Dashboard(FileSystemEventHandler): @@ -57,7 +57,7 @@ class Dashboard(FileSystemEventHandler): logdir = config.get_str('journaldir', default=config.default_journal_dir) logdir = logdir or config.default_journal_dir - if not isdir(logdir): + if not Path.is_dir(Path(logdir)): logger.info(f"No logdir, or it isn't a directory: {logdir=}") self.stop() return False @@ -87,7 +87,7 @@ class Dashboard(FileSystemEventHandler): if not self.observed and sys.platform == 'win32': logger.debug('Starting observer...') - self.observed = cast(BaseObserver, self.observer).schedule(self, self.currentdir) + self.observed = cast(BaseObserver, self.observer).schedule(self, self.currentdir) # type: ignore logger.debug('Done') logger.info(f'{(sys.platform != "win32") and "Polling" or "Monitoring"} Dashboard "{self.currentdir}"') @@ -160,11 +160,12 @@ class Dashboard(FileSystemEventHandler): def on_modified(self, event) -> None: """ - Watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows. + Watchdog callback - FileModifiedEvent on Windows. :param event: Watchdog event. """ - if event.is_directory or (isfile(event.src_path) and getsize(event.src_path)): + modpath = Path(event.src_path) + if event.is_directory or (modpath.is_file() and modpath.stat().st_size): # Can get on_modified events when the file is emptied self.process(event.src_path if not event.is_directory else None) @@ -177,7 +178,7 @@ class Dashboard(FileSystemEventHandler): if config.shutting_down: return try: - status_json_path = join(self.currentdir, 'Status.json') + status_json_path = Path(self.currentdir) / 'Status.json' with open(status_json_path, 'rb') as h: data = h.read().strip() if data: # Can be empty if polling while the file is being re-written diff --git a/debug_webserver.py b/debug_webserver.py index 87f7eaed..1471ec7d 100644 --- a/debug_webserver.py +++ b/debug_webserver.py @@ -4,21 +4,19 @@ from __future__ import annotations import gzip import json import pathlib -import tempfile import threading import zlib from http import server from typing import Any, Callable, Literal from urllib.parse import parse_qs - -from config import appname +import config from EDMCLogging import get_main_logger logger = get_main_logger() output_lock = threading.Lock() -output_data_path = pathlib.Path(tempfile.gettempdir()) / f'{appname}' / 'http_debug' -SAFE_TRANSLATE = str.maketrans({x: '_' for x in "!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`"}) +output_data_path = pathlib.Path(config.app_dir_path / 'logs' / 'http_debug') +SAFE_TRANSLATE = str.maketrans(dict.fromkeys("!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`", '_')) class LoggingHandler(server.BaseHTTPRequestHandler): diff --git a/docs/Licenses/WiXtoolset/LICENSE.TXT b/docs/Licenses/WiXtoolset/LICENSE.TXT deleted file mode 100644 index d4d316ef..00000000 --- a/docs/Licenses/WiXtoolset/LICENSE.TXT +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) .NET Foundation and contributors. -This software is released under the Microsoft Reciprocal License (MS-RL) (the "License"); you may not use the software except in compliance with the License. - -The text of the Microsoft Reciprocal License (MS-RL) can be found online at: - http://opensource.org/licenses/ms-rl - - -Microsoft Reciprocal License (MS-RL) - -This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. - -1. Definitions - The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. - A "contribution" is the original software, or any additions or changes to the software. - A "contributor" is any person that distributes its contribution under this license. - "Licensed patents" are a contributor's patent claims that read directly on its contribution. - -2. Grant of Rights - (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. - (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. - -3. Conditions and Limitations - (A) Reciprocal Grants- For any file you distribute that contains code from the software (in source code or binary format), you must provide recipients the source code to that file along with a copy of this license, which license will govern that file. You may license other files that are entirely your own work and do not contain code from the software under any terms you choose. - (B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. - (C) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. - (D) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. - (E) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. - (F) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. diff --git a/docs/Licenses/python-packages/pillow/LICENSE b/docs/Licenses/python-packages/pillow/LICENSE new file mode 100644 index 00000000..7990a6e5 --- /dev/null +++ b/docs/Licenses/python-packages/pillow/LICENSE @@ -0,0 +1,30 @@ +The Python Imaging Library (PIL) is + + Copyright Š 1997-2011 by Secret Labs AB + Copyright Š 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright Š 2010-2024 by Jeffrey A. Clark and contributors + +Like PIL, Pillow is licensed under the open source HPND License: + +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: + +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/Licenses/python-packages/psutil/LICENSE b/docs/Licenses/python-packages/psutil/LICENSE new file mode 100644 index 00000000..cff5eb74 --- /dev/null +++ b/docs/Licenses/python-packages/psutil/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the psutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/Licenses/python-packages/simplesystray/legalcode.txt b/docs/Licenses/python-packages/simplesystray/legalcode.txt new file mode 100644 index 00000000..da6ab6cc --- /dev/null +++ b/docs/Licenses/python-packages/simplesystray/legalcode.txt @@ -0,0 +1,396 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/docs/Translations.md b/docs/Translations.md index 959897f2..1de83c3b 100644 --- a/docs/Translations.md +++ b/docs/Translations.md @@ -8,15 +8,20 @@ Translations are handled on [OneSky](https://oneskyapp.com/), specifically in [t ### Setting it up in the code -#### Call `_(...)` +#### Call `tr.tl(...)` If you add any new strings that appear in the application UI, e.g. new configuration options, then you should specify them as: - _('Text that appears in UI') -`_()` is a special global function that then handles the translation, using its single argument, plus the configured language, to look up the appropriate text. + tr.tl('Text that appears in UI') + +In order to do this, you must add the following import: + +`from l10n import translations as tr` + +`tr.tl()` is a function that then handles the translation, using its single argument, plus the configured language, to look up the appropriate text. If you need to specify something in the text that shouldn't be translated then use the form: - _('Some text with a {WORD} not translated').format(WORD='word') + tr.tl('Some text with a {WORD} not translated').format(WORD='word') This way 'word' will always be used literally. #### Add a LANG comment @@ -28,8 +33,9 @@ end of the line in your usage**. If both comments exist, the one on the current line is preferred over the one above ```py +from l10n import translations as tr # LANG: this says stuff. -_('stuff') +tr.tl('stuff') ``` #### Edit `L10n/en.template` to add the phrase @@ -43,7 +49,7 @@ e.g. "Authentication successful" = "Authentication successful"; which matches with: - self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website + self.status['text'] = tr.tl('Authentication successful') # Successfully authenticated with the Frontier website and @@ -51,12 +57,12 @@ and "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name"; which matches with: - nb.Label(plugsframe, text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')).grid( # Help text in settings + nb.Label(plugsframe, text=tr.tl("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')).grid( # Help text in settings `{CR}` is handled in `l10n.py`, translating to a unicode `\n`. See the code in`l10n.py` for any other such special substitutions. You can even use other translations within a given string, e.g.: - _("One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.".format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')) + tr.tl("One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.".format(PLUGINS=tr.tl('Plugins'), FILE=tr.tl('File'), SETTINGS=tr.tl('Settings'), DISABLED='.disabled')) /* Popup body: Warning about plugins without Python 3.x support [EDMarketConnector.py] */ "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name."; diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py index ad90a084..3620e159 100644 --- a/docs/examples/click_counter/load.py +++ b/docs/examples/click_counter/load.py @@ -7,6 +7,7 @@ from __future__ import annotations import logging import tkinter as tk + import myNotebook as nb # noqa: N813 from config import appname, config @@ -47,7 +48,7 @@ class ClickCounter: """ self.on_preferences_closed("", False) # Save our prefs - def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> tk.Frame | None: + def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None: """ setup_preferences is called by plugin_prefs below. @@ -63,7 +64,7 @@ class ClickCounter: # setup our config in a "Click Count: number" nb.Label(frame, text='Click Count').grid(row=current_row) - nb.Entry(frame, textvariable=self.click_count).grid(row=current_row, column=1) + nb.EntryMenu(frame, textvariable=self.click_count).grid(row=current_row, column=1) current_row += 1 # Always increment our row counter, makes for far easier tkinter design. return frame @@ -126,7 +127,7 @@ def plugin_stop() -> None: return cc.on_unload() -def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> tk.Frame | None: +def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None: """ Handle preferences tab for the plugin. diff --git a/docs/examples/plugintest/load.py b/docs/examples/plugintest/load.py index 4aca1689..b494f436 100644 --- a/docs/examples/plugintest/load.py +++ b/docs/examples/plugintest/load.py @@ -3,10 +3,10 @@ """Plugin that tests that modules we bundle for plugins are present and working.""" import logging -import os import shutil import sqlite3 import zipfile +from pathlib import Path import semantic_version from SubA import SubA @@ -14,7 +14,7 @@ from SubA import SubA from config import appname, appversion, config # This could also be returned from plugin_start3() -plugin_name = os.path.basename(os.path.dirname(__file__)) +plugin_name = Path(__file__).resolve().parent.name # Logger per found plugin, so the folder name is included in # the logging format. @@ -49,17 +49,17 @@ class PluginTest: def __init__(self, directory: str): logger.debug(f'directory = "{directory}') - dbfile = os.path.join(directory, this.DBFILE) + dbfile = Path(directory) / this.DBFILE # Test 'import zipfile' - with zipfile.ZipFile(dbfile + '.zip', 'w') as zip: - if os.path.exists(dbfile): + with zipfile.ZipFile(str(dbfile) + '.zip', 'w') as zip: + if dbfile.exists(): zip.write(dbfile) zip.close() # Testing 'import shutil' - if os.path.exists(dbfile): - shutil.copyfile(dbfile, dbfile + '.bak') + if dbfile.exists(): + shutil.copyfile(dbfile, str(dbfile) + '.bak') # Testing 'import sqlite3' self.sqlconn = sqlite3.connect(dbfile) diff --git a/edmarketconnector.xml b/edmarketconnector.xml deleted file mode 100644 index a4187153..00000000 --- a/edmarketconnector.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - E:D Market Connector - https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml - Most recent changes with links to updates. - - - - - Release 3.44 - - h2 { font-size: 105%; } -

Release 3.44

-

CHANGE OF MAINTAINER

-

Due to a lack of time to give the project the attention it needs Marginal has handed over ownership of the EDMarketConnector GitHub repository to the EDCD (Elite Dangerous Community Developers) organisation.

-

Initially Athanasius will now be responsible for maintaining the code, including addressing any Pull Requests and Issues, and making releases. Unfortunately he has no access to hardware running MacOS so can't easily generate builds for that platform or test them. So for the time being releases will be for Windows 10 only. MacOS users are advised to look into running from source (see the github README).

-

Going forwards the intention is to move to the python 3.7 code as soon as possible. To facilitate this there will be one more python 2.7 release in addition to this one, with the main aim of that being to add code to alert the user about any plugins they use that have apparently not been updated to run under python 3.7.

-

See the project GitHub repository's README.md for further information.

-
    -
  • Version increased to 3.4.4.0 / 3.44.
  • -
  • URL the application checks for updates changed to point to github,
  • -
- ]]> -
- -
- - - - - Release 4.2.3 - - body { font-family:"Segoe UI","Tahoma"; font-size: 75%; } h2 { font-family:"Segoe UI","Tahoma"; font-size: 105%; } - -

Release 4.2.3

-

This release mostly addresses an issue when Frontier Authorisation gets stuck -on 'Logging in...' despite completing the authorisation on the Frontier -website.

-
    -
  • -

    Allow edmc... argument to EDMarketConnector.exe. This should only be -necessary when something has prevented your web browser from invoking the -edmc protocol via DDE.

    -

    If you were encountering the 'Logging in...' issue and still do with this -release then please try running the application via the new -EDMarketConnector - localserver-auth.bat file in the installation -directory.

    -

    This simply runs EDMarketConnector.exe with the ---force-localserver-for-auth command-line argument. This forces the code -to setup and use a webserver on a random port on localhost for the -Frontier Authorisation callback, the same way it already works on -non-Windows platforms.

    -
  • -
  • -

    Add Korean translation to both the application and the installer.

    -
  • -
- - -

Release 4.2.2

-

This release contains a minor bug-fix, actually properly checking a station's -ships list before operating on it.

-
    -
  • Check that ships['shipuard_list'] is a dict before trying to use -.values() on it. This fixes the issue with seeing list object has no attribute values in the application status line.
  • -
- -

Release 4.2.1

-

This is a bug-fix release.

-
    -
  • -

    Updated translations. Thanks once again to all those contributing as per -Translations.

    -
  • -
  • -

    PLUGINS.md: Clarify when CargoJSON is populated.

    -
  • -
  • -

    macOS: pip install -r requirements.txt will now include pyobjc so that -running this application works at all. Check the updated Running from -source -for some advice if attempting to run on macOS.

    -
  • -
  • -

    JournalLock: Handle when the Journal directory isn't set at all, rather than -erroring. Fixes #910 - Not launching (Linux).

    -
  • -
  • -

    Extra logging added to track down cause of #909 - Authentication not possible (PC) -. The debug log file might now indicate what's wrong, or we might need -you to run

    -
    "c:\Program Files (x86)\EDMarketConnector/EDMarketConnector.exe" --trace
    -
    -

    in order to increase the log level and gather some extra information. -Caution is advised if sharing a --trace log file as it will now contain -some of the actual auth data returned from Frontier.

    -
  • -
  • -

    Ensure that 'Save Raw Data' will work. Fixes #908 - Raw export of CAPI data broken.

    -
  • -
  • -

    Prevent EDDN plugin from erroring when we determine if the commander has -Horizons. Fixes #907 - Modules is a list not a dict on damaged stations

    -
  • -
- - -

Release 4.2.0

-

This release increases the Minor version due to the major change in how -multiple-instance checking is done.

-
    -
  • -

    Adds Steam and Epic to the list of "audiences" in the Frontier Auth callout -so that you can authorise using those accounts, rather than their associated -Frontier Account details.

    -
  • -
  • -

    New status message "CAPI: No commander data returned" if a /profile -request has no commander in the returned data. This can happen if you -literally haven't yet created a Commander on the account. Previously you'd -get a confusing 'commander' message shown.

    -
  • -
  • -

    Changes the "is there another process already running?" check to be based on -a lockfile in the configured Journals directory. The name of this file is -edmc-journal-lock.txt and upon successful locking it will contain text -like:

    -
    Path: <configured path to your Journals>
    -PID: <process ID of the application>
    -
    -

    The lock will be released and applied to the new directory if you change it -via Settings > Configuration. If the new location is already locked you'll -get a 'Retry/Ignore?' pop-up.

    -

    For most users things will operate no differently, although note that the -multiple instance check does now apply to platforms other than Windows.

    -

    For anyone wanting to run multiple instances of the program this is now -possible via:

    -

    runas /user:<USER> "\"c:\Program Files (x86)\EDMarketConnector\EDMarketConnector.exe\" --force-localserver-for-auth"

    -

    If anything has messed with the backslash characters there then know that you -need to have " (double-quote) around the entire command (path to program .exe -and any extra arguments), and as a result need to place a backslash before -any double-quote characters in the command (such as around the space-including -path to the program).

    -

    I've verified it renders correctly on GitHub.

    -

    The old check was based solely on there being a window present with the title -we expect. This prevented using runas /user:SOMEUSER ... to run a second -copy of the application, as the resulting window would still be within the -same desktop environment and thus be found in the check.

    -

    The new method does assume that the Journals directory is writable by the -user we're running as. This might not be true in the case of sharing the -file system to another host in a read-only manner. If we fail to open the -lock file read-write then the application aborts the checks and will simply -continue running as normal.

    -

    Note that any single instance of EDMarketConnector.exe will still only monitor -and act upon the latest Journal file in the configured location. If you run -Elite Dangerous for another Commander then the application will want to start -monitoring that separate Commander. See wiki:Troubleshooting#i-run-two-instances-of-ed-simultaneously-but-i-cant-run-two-instances-of-edmc -which will be updated when this change is in a full release.

    -
  • -
  • -

    Adds the command-line argument --force-localserver-for-auth. This forces -using a local webserver for the Frontier Auth callback. This should be used -when running multiple instances of the application for all instances -else there's no guarantee of the edmc:// protocol callback reaching the -correct process and Frontier Auth will fail.

    -
  • -
  • -

    Adds the command-line argument --suppress-dupe-process-popup to exit -without showing the warning popup in the case that EDMarketConnector found -another process already running.

    -

    This can be useful if wanting to blindly run both EDMC and the game from a -batch file or similar.

    -
  • -
- - - -

Release 4.1.6

-

We might have finally found the cause of the application hangs during shutdown. -Note that this became easier to track down due to the downtime -for migration of www.edsm.net around 2021-01-11. Before these fixes EDSM's -API not being available would cause an EDMC hang on shutdown.

-
    -
  • -

    We've applied extra paranoia to some of the application shutdown code to -ensure we're not still trying to handle journal events during this sequence.

    -

    We also re-ordered the shutdown sequence, which might help avoid the shutdown -hang.

    -

    If you encounter a shutdown hang then please add a comment and log files to -Application can leave a zombie process on shutdown #678 -to help us track down the cause and fix it.

    -
  • -
  • -

    We now avoid making Tk event_generate() calls whilst the appliction is -shutting down.

    -
  • -
  • -

    Plugins should actively avoid making any sort of Tk event_generate() call -during application shutdown.

    -

    This means using if not config.shutting_down: to gate any code in worker -threads that might attempt this. Also, be sure you're not attempting such -in your plugin_stop() function.

    -

    See plugins/edsm.py and plugins/inara.py for example of the usage.

    -
  • -
  • -

    Any use of plug.show_error() won't actually change the UI status line -during shutdown, but the text you tried to show will be logged instead.

    -
  • -
  • -

    Cargo tracking will now correctly count all instances of the same type of -cargo for different missions. Previously it only counted the cargo for -the last mission requiring that cargo type, as found in Cargo.json.

    -
  • -
  • -

    The loaded contents of Cargo.json can now be found in monitor.state['CargoJSON']. -monitor.state is what is passed to plugins as state in the -journal_entry() call.

    -
  • -
  • -

    Our logging code should now cope with logging from a property.

    -
  • -
  • -

    Logging from any name-mangled method should now work properly.

    -
  • -
  • -

    Miscellaneous updates to PLUGINS.md - mostly to clarify some things.

    -
  • -
- - ]]> -
- -
- -
-
diff --git a/edmc_data.py b/edmc_data.py index c628ae8d..1f68dd77 100644 --- a/edmc_data.py +++ b/edmc_data.py @@ -4,7 +4,6 @@ Static data. For easy reference any variable should be prefixed with the name of the file it was either in originally, or where the primary code utilising it is. """ -from collections import OrderedDict # Map numeric 'demand/supply brackets' to the names as shown in-game. commodity_bracketmap = { @@ -57,18 +56,21 @@ edshipyard_slot_map = { # Map API module names to in-game names -outfitting_armour_map = OrderedDict([ - ('grade1', 'Lightweight Alloy'), - ('grade2', 'Reinforced Alloy'), - ('grade3', 'Military Grade Composite'), - ('mirrored', 'Mirrored Surface Composite'), - ('reactive', 'Reactive Surface Composite'), -]) +outfitting_armour_map = { + 'grade1': 'Lightweight Alloy', + 'grade2': 'Reinforced Alloy', + 'grade3': 'Military Grade Composite', + 'mirrored': 'Mirrored Surface Composite', + 'reactive': 'Reactive Surface Composite', +} + outfitting_weapon_map = { 'advancedtorppylon': 'Torpedo Pylon', 'atdumbfiremissile': 'AX Missile Rack', 'atmulticannon': 'AX Multi-Cannon', + ('atmulticannon', 'v2'): 'Enhanced AX Multi-Cannon', + ('atdumbfiremissile', 'v2'): 'Enhanced AX Missile Rack', 'basicmissilerack': 'Seeker Missile Rack', 'beamlaser': 'Beam Laser', ('beamlaser', 'heat'): 'Retributor Beam Laser', @@ -88,6 +90,8 @@ outfitting_weapon_map = { 'mining_abrblstr': 'Abrasion Blaster', 'mining_seismchrgwarhd': 'Seismic Charge Launcher', 'mining_subsurfdispmisle': 'Sub-Surface Displacement Missile', + 'human_extraction': 'Sub-Surface Extraction Missile', + 'atventdisruptorpylon': 'Guardian Nanite Torpedo Pylon', 'mininglaser': 'Mining Laser', ('mininglaser', 'advanced'): 'Mining Lance Beam Laser', 'multicannon': 'Multi-Cannon', @@ -209,7 +213,9 @@ outfitting_weaponrating_map = { 'hpt_mininglaser_turret_small': 'D', 'hpt_mininglaser_turret_medium': 'D', 'hpt_multicannon_fixed_small': 'F', + 'hpt_multicannon_fixed_small_advanced': 'F', 'hpt_multicannon_fixed_medium': 'E', + 'hpt_multicannon_fixed_medium_advanced': 'E', 'hpt_multicannon_fixed_large': 'C', 'hpt_multicannon_fixed_huge': 'A', 'hpt_multicannon_gimbal_small': 'G', @@ -266,6 +272,11 @@ outfitting_weaponrating_map = { 'hpt_slugshot_turret_medium': 'D', 'hpt_slugshot_turret_large': 'C', 'hpt_xenoscannermk2_basic_tiny': '?', + 'hpt_atmulticannon_gimbal_large': 'C', + 'hpt_atmulticannon_gimbal_medium': 'E', + 'hpt_human_extraction_fixed_medium': 'B', + 'hpt_atventdisruptorpylon_fixed_medium': 'I', + 'hpt_atventdisruptorpylon_fixed_large': 'I', } # Old standard weapon variants @@ -278,13 +289,14 @@ outfitting_weaponoldvariant_map = { } outfitting_countermeasure_map = { - 'antiunknownshutdown': ('Shutdown Field Neutraliser', 'F'), - 'chafflauncher': ('Chaff Launcher', 'I'), - 'electroniccountermeasure': ('Electronic Countermeasure', 'F'), - 'heatsinklauncher': ('Heat Sink Launcher', 'I'), - 'plasmapointdefence': ('Point Defence', 'I'), - 'xenoscanner': ('Xeno Scanner', 'E'), - 'xenoscannermk2': ('Unknown Xeno Scanner Mk II', '?'), + 'antiunknownshutdown': ('Shutdown Field Neutraliser', 'F'), + ('antiunknownshutdown', 'v2'): ('Thargoid Pulse Neutraliser', 'E'), + 'chafflauncher': ('Chaff Launcher', 'I'), + 'electroniccountermeasure': ('Electronic Countermeasure', 'F'), + 'heatsinklauncher': ('Heat Sink Launcher', 'I'), + 'plasmapointdefence': ('Point Defence', 'I'), + 'xenoscanner': ('Xeno Scanner', 'E'), + 'xenoscannermk2': ('Unknown Xeno Scanner Mk II', '?'), } outfitting_utility_map = { @@ -347,6 +359,7 @@ outfitting_standard_map = { 'guardianpowerdistributor': 'Guardian Hybrid Power Distributor', 'guardianpowerplant': 'Guardian Hybrid Power Plant', 'hyperdrive': 'Frame Shift Drive', + ('hyperdrive', 'overcharge'): 'Frame Shift Drive (SCO)', 'lifesupport': 'Life Support', # 'planetapproachsuite': handled separately 'powerdistributor': 'Power Distributor', @@ -376,6 +389,11 @@ outfitting_internal_map = { 'refinery': 'Refinery', 'recon': 'Recon Limpet Controller', 'repair': 'Repair Limpet Controller', + 'rescue': 'Rescue Limpet Controller', + 'mining': 'Mining Multi Limpet Controller', + 'xeno': 'Xeno Multi Limpet Controller', + 'operations': 'Operations Multi Limpet Controller', + 'universal': 'Universal Multi Limpet Controller', 'repairer': 'Auto Field-Maintenance Unit', 'resourcesiphon': 'Hatch Breaker Limpet Controller', 'shieldcellbank': 'Shield Cell Bank', @@ -383,6 +401,7 @@ outfitting_internal_map = { ('shieldgenerator', 'fast'): 'Bi-Weave Shield Generator', ('shieldgenerator', 'strong'): 'Prismatic Shield Generator', 'unkvesselresearch': 'Research Limpet Controller', + 'expmodulestabiliser': 'Experimental Weapon Stabiliser', } # Dashboard Flags constants @@ -460,6 +479,7 @@ ship_name_map = { 'belugaliner': 'Beluga Liner', 'cobramkiii': 'Cobra MkIII', 'cobramkiv': 'Cobra MkIV', + 'cobramkv': 'Cobra MkV', 'clipper': 'Panther Clipper', 'cutter': 'Imperial Cutter', 'diamondback': 'Diamondback Scout', @@ -482,13 +502,16 @@ ship_name_map = { 'krait_mkii': 'Krait MkII', 'krait_light': 'Krait Phantom', 'mamba': 'Mamba', + 'mandalay': 'Mandalay', 'orca': 'Orca', 'python': 'Python', + 'python_nx': 'Python Mk II', 'scout': 'Taipan Fighter', 'sidewinder': 'Sidewinder', 'testbuggy': 'Scarab', 'type6': 'Type-6 Transporter', 'type7': 'Type-7 Transporter', + 'type8': 'Type-8 Transporter', 'type9': 'Type-9 Heavy', 'type9_military': 'Type-10 Defender', 'typex': 'Alliance Chieftain', diff --git a/edshipyard.py b/edshipyard.py index 5d14009c..2c29959f 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -24,7 +24,7 @@ __Module = dict[str, Union[str, list[str]]] # Have to keep old-style here for c ship_map = ship_name_map.copy() # Ship masses -ships_file = config.respath_path / "resources" / "ships.json" +ships_file = config.respath_path / "ships.json" with open(ships_file, encoding="utf-8") as ships_file_handle: ships = json.load(ships_file_handle) @@ -106,7 +106,7 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 else: name = module['name'] # type: ignore - if name == 'Frame Shift Drive': + if name == 'Frame Shift Drive' or name == 'Frame Shift Drive (SCO)': fsd = module # save for range calculation if mods.get('OutfittingFieldType_FSDOptimalMass'): @@ -167,15 +167,19 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 try: mass += ships[ship_name_map[data['ship']['name'].lower()]]['hullMass'] string += f'Mass : {mass:.2f} T empty\n {mass + fuel + cargo:.2f} T full\n' + maxfuel = fsd.get('maxfuel', 0) # type: ignore + fuelmul = fsd.get('fuelmul', 0) # type: ignore - multiplier = pow(min(fuel, fsd['maxfuel']) / fsd['fuelmul'], 1.0 # type: ignore - / fsd['fuelpower']) * fsd['optmass'] # type: ignore - - range_unladen = multiplier / (mass + fuel) + jumpboost - range_laden = multiplier / (mass + fuel + cargo) + jumpboost - # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with - # a locale that uses comma for decimal separator. - string += f'Range : {range_unladen:.2f} LY unladen\n {range_laden:.2f} LY laden\n' + try: + multiplier = pow(min(fuel, maxfuel) / fuelmul, 1.0 / fsd['fuelpower']) * fsd['optmass'] # type: ignore + range_unladen = multiplier / (mass + fuel) + jumpboost + range_laden = multiplier / (mass + fuel + cargo) + jumpboost + # As of 2021-04-07 edsy.org says text import not yet implemented, so ignore the possible issue with + # a locale that uses comma for decimal separator. + except ZeroDivisionError: + range_unladen = range_laden = 0.0 + string += (f'Range : {range_unladen:.2f} LY unladen\n' + f' {range_laden:.2f} LY laden\n') except Exception: if __debug__: diff --git a/hotkey/__init__.py b/hotkey/__init__.py index e7515807..313415f9 100644 --- a/hotkey/__init__.py +++ b/hotkey/__init__.py @@ -76,10 +76,6 @@ def get_hotkeymgr() -> AbstractHotkeyMgr: :return: Appropriate class instance. :raises ValueError: If unsupported platform. """ - if sys.platform == 'darwin': - from hotkey.darwin import MacHotkeyMgr - return MacHotkeyMgr() - if sys.platform == 'win32': from hotkey.windows import WindowsHotkeyMgr return WindowsHotkeyMgr() diff --git a/hotkey/darwin.py b/hotkey/darwin.py deleted file mode 100644 index 63f9d132..00000000 --- a/hotkey/darwin.py +++ /dev/null @@ -1,276 +0,0 @@ -"""darwin/macOS implementation of hotkey.AbstractHotkeyMgr.""" -from __future__ import annotations - -import pathlib -import sys -import tkinter as tk -from typing import Callable -assert sys.platform == 'darwin' - -import objc -from AppKit import ( - NSAlternateKeyMask, NSApplication, NSBeep, NSClearLineFunctionKey, NSCommandKeyMask, NSControlKeyMask, - NSDeleteFunctionKey, NSDeviceIndependentModifierFlagsMask, NSEvent, NSF1FunctionKey, NSF35FunctionKey, - NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace -) - -from config import config -from EDMCLogging import get_main_logger -from hotkey import AbstractHotkeyMgr - -logger = get_main_logger() - - -class MacHotkeyMgr(AbstractHotkeyMgr): - """Hot key management.""" - - POLL = 250 - # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes - DISPLAY = { - 0x03: u'⌅', 0x09: u'â‡Ĩ', 0xd: u'↩', 0x19: u'⇤', 0x1b: u'esc', 0x20: u'⏘', 0x7f: u'âŒĢ', - 0xf700: u'↑', 0xf701: u'↓', 0xf702: u'←', 0xf703: u'→', - 0xf727: u'Ins', - 0xf728: u'âŒĻ', 0xf729: u'↖', 0xf72a: u'Fn', 0xf72b: u'↘', - 0xf72c: u'⇞', 0xf72d: u'⇟', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock', - 0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset', - 0xf739: u'⌧', - } - (ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3) - - def __init__(self): - self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \ - | NSNumericPadKeyMask - self.root: tk.Tk - - self.keycode = 0 - self.modifiers = 0 - self.activated = False - self.observer = None - - self.acquire_key = 0 - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - - self.tkProcessKeyEvent_old: Callable - - self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_( - pathlib.Path(config.respath_path) / 'snd_good.wav', False - ) - self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_( - pathlib.Path(config.respath_path) / 'snd_bad.wav', False - ) - - def register(self, root: tk.Tk, keycode: int, modifiers: int) -> None: - """ - Register current hotkey for monitoring. - - :param root: parent window. - :param keycode: Key to monitor. - :param modifiers: Any modifiers to take into account. - """ - self.root = root - self.keycode = keycode - self.modifiers = modifiers - self.activated = False - - if keycode: - if not self.observer: - self.root.after_idle(self._observe) - self.root.after(MacHotkeyMgr.POLL, self._poll) - - # Monkey-patch tk (tkMacOSXKeyEvent.c) - if not callable(self.tkProcessKeyEvent_old): - sel = b'tkProcessKeyEvent:' - cls = NSApplication.sharedApplication().class__() # type: ignore - self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) # type: ignore - newmethod = objc.selector( # type: ignore - self.tkProcessKeyEvent, - selector=self.tkProcessKeyEvent_old.selector, - signature=self.tkProcessKeyEvent_old.signature - ) - objc.classAddMethod(cls, sel, newmethod) # type: ignore - - def tkProcessKeyEvent(self, cls, the_event): # noqa: N802 - """ - Monkey-patch tk (tkMacOSXKeyEvent.c). - - - workaround crash on OSX 10.9 & 10.10 on seeing a composing character - - notice when modifier key state changes - - keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey - - (Would like to use a decorator but need to ensure the application is created before this is installed) - :param cls: ??? - :param the_event: tk event - :return: ??? - """ - if self.acquire_state: - if the_event.type() == NSFlagsChanged: - self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask - self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW - # suppress the event by not chaining the old function - return the_event - - if the_event.type() in (NSKeyDown, NSKeyUp): - c = the_event.charactersIgnoringModifiers() - self.acquire_key = (c and ord(c[0]) or 0) | \ - (the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask) - self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW - # suppress the event by not chaining the old function - return the_event - - # replace empty characters with charactersIgnoringModifiers to avoid crash - elif the_event.type() in (NSKeyDown, NSKeyUp) and not the_event.characters(): - the_event = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_( # noqa: E501 - # noqa: E501 - the_event.type(), - the_event.locationInWindow(), - the_event.modifierFlags(), - the_event.timestamp(), - the_event.windowNumber(), - the_event.context(), - the_event.charactersIgnoringModifiers(), - the_event.charactersIgnoringModifiers(), - the_event.isARepeat(), - the_event.keyCode() - ) - return self.tkProcessKeyEvent_old(cls, the_event) - - def _observe(self): - # Must be called after root.mainloop() so that the app's message loop has been created - self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler) - - def _poll(self): - if config.shutting_down: - return - - # No way of signalling to Tkinter from within the callback handler block that doesn't - # cause Python to crash, so poll. - if self.activated: - self.activated = False - self.root.event_generate('<>', when="tail") - - if self.keycode or self.modifiers: - self.root.after(MacHotkeyMgr.POLL, self._poll) - - def unregister(self) -> None: - """Remove hotkey registration.""" - self.keycode = 0 - self.modifiers = 0 - - @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) - def _handler(self, event) -> None: - # use event.charactersIgnoringModifiers to handle composing characters like Alt-e - if ( - (event.modifierFlags() & self.MODIFIERMASK) == self.modifiers - and ord(event.charactersIgnoringModifiers()[0]) == self.keycode - ): - if config.get_int('hotkey_always'): - self.activated = True - - else: # Only trigger if game client is front process - front = NSWorkspace.sharedWorkspace().frontmostApplication() - if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': - self.activated = True - - def acquire_start(self) -> None: - """Start acquiring hotkey state via polling.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after_idle(self._acquire_poll) - - def acquire_stop(self) -> None: - """Stop acquiring hotkey state.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - - def _acquire_poll(self) -> None: - """Perform a poll of current hotkey state.""" - if config.shutting_down: - return - - # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't - # cause Python to crash, so poll. - if self.acquire_state: - if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW: - # Abuse tkEvent's keycode field to hold our acquired key & modifier - self.root.event_generate('', keycode=self.acquire_key) - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after(50, self._acquire_poll) - - def fromevent(self, event) -> bool | tuple | None: - """ - Return configuration (keycode, modifiers) or None=clear or False=retain previous. - - :param event: tk event ? - :return: False to retain previous, None to not use, else (keycode, modifiers) - """ - (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() - if ( - keycode - and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask)) - ): - if keycode == 0x1b: # Esc = retain previous - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return False - - # BkSp, Del, Clear = clear hotkey - if keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None - - # don't allow keys needed for typing in System Map - if keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: - NSBeep() - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None - - return keycode, modifiers - - def display(self, keycode, modifiers) -> str: - """ - Return displayable form of given hotkey + modifiers. - - :param keycode: - :param modifiers: - :return: string form - """ - text = '' - if modifiers & NSControlKeyMask: - text += u'⌃' - - if modifiers & NSAlternateKeyMask: - text += u'âŒĨ' - - if modifiers & NSShiftKeyMask: - text += u'⇧' - - if modifiers & NSCommandKeyMask: - text += u'⌘' - - if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: - text += u'№' - - if not keycode: - pass - - elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): - text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' - - elif keycode in MacHotkeyMgr.DISPLAY: # specials - text += MacHotkeyMgr.DISPLAY[keycode] - - elif keycode < 0x20: # control keys - text += chr(keycode + 0x40) - - elif keycode < 0xf700: # key char - text += chr(keycode).upper() - - else: - text += u'⁈' - - return text - - def play_good(self): - """Play the 'good' sound.""" - self.snd_good.play() - - def play_bad(self): - """Play the 'bad' sound.""" - self.snd_bad.play() diff --git a/hotkey/windows.py b/hotkey/windows.py index f21a24b2..ddccc333 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -8,7 +8,11 @@ import sys import threading import tkinter as tk import winsound -from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD +from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD, HWND, BOOL, UINT +import pywintypes +import win32api +import win32gui +import win32con from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -17,53 +21,22 @@ assert sys.platform == 'win32' logger = get_main_logger() -RegisterHotKey = ctypes.windll.user32.RegisterHotKey -UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey -MOD_ALT = 0x0001 -MOD_CONTROL = 0x0002 -MOD_SHIFT = 0x0004 -MOD_WIN = 0x0008 +UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon +UnregisterHotKey.argtypes = [HWND, ctypes.c_int] +UnregisterHotKey.restype = BOOL + MOD_NOREPEAT = 0x4000 - -GetMessage = ctypes.windll.user32.GetMessageW -TranslateMessage = ctypes.windll.user32.TranslateMessage -DispatchMessage = ctypes.windll.user32.DispatchMessageW -PostThreadMessage = ctypes.windll.user32.PostThreadMessageW -WM_QUIT = 0x0012 -WM_HOTKEY = 0x0312 -WM_APP = 0x8000 -WM_SND_GOOD = WM_APP + 1 -WM_SND_BAD = WM_APP + 2 - -GetKeyState = ctypes.windll.user32.GetKeyState -MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW -VK_BACK = 0x08 -VK_CLEAR = 0x0c -VK_RETURN = 0x0d -VK_SHIFT = 0x10 -VK_CONTROL = 0x11 -VK_MENU = 0x12 -VK_CAPITAL = 0x14 -VK_MODECHANGE = 0x1f -VK_ESCAPE = 0x1b -VK_SPACE = 0x20 -VK_DELETE = 0x2e -VK_LWIN = 0x5b -VK_RWIN = 0x5c -VK_NUMPAD0 = 0x60 -VK_DIVIDE = 0x6f -VK_F1 = 0x70 -VK_F24 = 0x87 +WM_SND_GOOD = win32con.WM_APP + 1 +WM_SND_BAD = win32con.WM_APP + 2 VK_OEM_MINUS = 0xbd -VK_NUMLOCK = 0x90 -VK_SCROLL = 0x91 -VK_PROCESSKEY = 0xe5 -VK_OEM_CLEAR = 0xfe -GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow -GetWindowText = ctypes.windll.user32.GetWindowTextW -GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] -GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW +# VirtualKey mapping values +# +MAPVK_VK_TO_VSC = 0 +MAPVK_VSC_TO_VK = 1 +MAPVK_VK_TO_CHAR = 2 +MAPVK_VSC_TO_VK_EX = 3 +MAPVK_VK_TO_VSC_EX = 4 def window_title(h) -> str: @@ -74,11 +47,7 @@ def window_title(h) -> str: :return: Window title. """ if h: - title_length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(title_length) - if GetWindowText(h, buf, title_length): - return buf.value - + return win32gui.GetWindowText(h) return '' @@ -138,6 +107,7 @@ class INPUT(ctypes.Structure): SendInput = ctypes.windll.user32.SendInput SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] +SendInput.restype = UINT INPUT_MOUSE = 0 INPUT_KEYBOARD = 1 @@ -197,7 +167,7 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): logger.debug('Thread is/was running') self.thread = None # type: ignore logger.debug('Telling thread WM_QUIT') - PostThreadMessage(thread.ident, WM_QUIT, 0, 0) + win32gui.PostThreadMessage(thread.ident, win32con.WM_QUIT, 0, 0) logger.debug('Joining thread') thread.join() # Wait for it to unregister hotkey and quit @@ -210,8 +180,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): """Handle hotkeys.""" logger.debug('Begin...') # Hotkey must be registered by the thread that handles it - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We're not the right thread?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We're not the right thread?") self.thread = None # type: ignore return @@ -219,14 +191,14 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): msg = MSG() logger.debug('Entering GetMessage() loop...') - while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: + while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0: logger.debug('Got message') - if msg.message == WM_HOTKEY: + if msg.message == win32con.WM_HOTKEY: logger.debug('WM_HOTKEY') if ( - config.get_int('hotkey_always') - or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') + config.get_int('hotkey_always') + or window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous') ): if not config.shutting_down: logger.debug('Sending event <>') @@ -236,8 +208,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): logger.debug('Passing key on') UnregisterHotKey(None, 1) SendInput(1, fake, ctypes.sizeof(INPUT)) - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We aren't registered for this ?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We aren't registered for this ?") break elif msg.message == WM_SND_GOOD: @@ -250,8 +224,8 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): else: logger.debug('Something else') - TranslateMessage(ctypes.byref(msg)) - DispatchMessage(ctypes.byref(msg)) + win32gui.TranslateMessage(ctypes.byref(msg)) + win32gui.DispatchMessage(ctypes.byref(msg)) logger.debug('Exited GetMessage() loop.') UnregisterHotKey(None, 1) @@ -277,40 +251,42 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): :param event: tk event ? :return: False to retain previous, None to not use, else (keycode, modifiers) """ - modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + modifiers = ((win32api.GetKeyState(win32con.VK_MENU) & 0x8000) and win32con.MOD_ALT) \ + | ((win32api.GetKeyState(win32con.VK_CONTROL) & 0x8000) and win32con.MOD_CONTROL) \ + | ((win32api.GetKeyState(win32con.VK_SHIFT) & 0x8000) and win32con.MOD_SHIFT) \ + | ((win32api.GetKeyState(win32con.VK_LWIN) & 0x8000) and win32con.MOD_WIN) \ + | ((win32api.GetKeyState(win32con.VK_RWIN) & 0x8000) and win32con.MOD_WIN) keycode = event.keycode - if keycode in [VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN]: + if keycode in (win32con.VK_SHIFT, win32con.VK_CONTROL, win32con.VK_MENU, win32con.VK_LWIN, win32con.VK_RWIN): return 0, modifiers if not modifiers: - if keycode == VK_ESCAPE: # Esc = retain previous + if keycode == win32con.VK_ESCAPE: # Esc = retain previous return False - if keycode in [VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR]: # BkSp, Del, Clear = clear hotkey + if keycode in (win32con.VK_BACK, win32con.VK_DELETE, + win32con.VK_CLEAR, win32con.VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey return None if ( - keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord('Z') + keycode in (win32con.VK_RETURN, win32con.VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') ): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None - if (keycode in [VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY] - or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys + if (keycode in (win32con.VK_NUMLOCK, win32con.VK_SCROLL, win32con.VK_PROCESSKEY) + or win32con.VK_CAPITAL <= keycode <= win32con.VK_MODECHANGE): # ignore unmodified mode switch keys return 0, modifiers # See if the keycode is usable and available - if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): + try: + win32gui.RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode) UnregisterHotKey(None, 2) return keycode, modifiers - - winsound.MessageBeep() - return None + except pywintypes.error: + winsound.MessageBeep() + return None def display(self, keycode, modifiers) -> str: """ @@ -321,32 +297,32 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): :return: string form """ text = '' - if modifiers & MOD_WIN: + if modifiers & win32con.MOD_WIN: text += '❖+' - if modifiers & MOD_CONTROL: + if modifiers & win32con.MOD_CONTROL: text += 'Ctrl+' - if modifiers & MOD_ALT: + if modifiers & win32con.MOD_ALT: text += 'Alt+' - if modifiers & MOD_SHIFT: + if modifiers & win32con.MOD_SHIFT: text += '⇧+' - if VK_NUMPAD0 <= keycode <= VK_DIVIDE: + if win32con.VK_NUMPAD0 <= keycode <= win32con.VK_DIVIDE: text += '№' if not keycode: pass - elif VK_F1 <= keycode <= VK_F24: - text += f'F{keycode + 1 - VK_F1}' + elif win32con.VK_F1 <= keycode <= win32con.VK_F24: + text += f'F{keycode + 1 - win32con.VK_F1}' elif keycode in WindowsHotkeyMgr.DISPLAY: # specials text += WindowsHotkeyMgr.DISPLAY[keycode] else: - c = MapVirtualKey(keycode, 2) # printable ? + c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR) if not c: # oops not printable text += '⁈' @@ -361,9 +337,9 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): def play_good(self) -> None: """Play the 'good' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) def play_bad(self) -> None: """Play the 'bad' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) diff --git a/img/Balance.png b/img/Balance.png index 8696f44c..e5d0c464 100644 Binary files a/img/Balance.png and b/img/Balance.png differ diff --git a/img/UI-Athanasius-101-odyssey-docked-onfoot-cooled-down-l.png b/img/UI-Athanasius-101-odyssey-docked-onfoot-cooled-down-l.png index b2f8d68a..d3d60fad 100644 Binary files a/img/UI-Athanasius-101-odyssey-docked-onfoot-cooled-down-l.png and b/img/UI-Athanasius-101-odyssey-docked-onfoot-cooled-down-l.png differ diff --git a/img/UI-annotated.png b/img/UI-annotated.png index f4efc003..8f8b0e81 100644 Binary files a/img/UI-annotated.png and b/img/UI-annotated.png differ diff --git a/img/win.png b/img/win.png index 383ea911..44c43d1c 100644 Binary files a/img/win.png and b/img/win.png differ diff --git a/img/win_dark.png b/img/win_dark.png index 2b8608d2..90c7e931 100644 Binary files a/img/win_dark.png and b/img/win_dark.png differ diff --git a/img/win_transparent.png b/img/win_transparent.png index b4ff49d9..c450c0ef 100644 Binary files a/img/win_transparent.png and b/img/win_transparent.png differ diff --git a/installer.py b/installer.py index 754b3373..1ee79ddb 100644 --- a/installer.py +++ b/installer.py @@ -5,23 +5,23 @@ Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. """ -import os import subprocess +from pathlib import Path from build import build -def run_inno_setup_installer(iss_path: str) -> None: +def run_inno_setup_installer(iss_path: Path) -> None: """Run the Inno installer, building the installation exe.""" # Get the path to the Inno Setup compiler (iscc.exe) (Currently set to default path) - inno_setup_compiler_path: str = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" + inno_setup_compiler_path = Path("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe") # Check if the Inno Setup compiler executable exists - if not os.path.isfile(inno_setup_compiler_path): + if not inno_setup_compiler_path.exists(): print(f"Error: Inno Setup compiler not found at '{inno_setup_compiler_path}'.") return # Check if the provided .iss file exists - if not os.path.isfile(iss_file_path): + if not iss_file_path.exists(): print(f"Error: The provided .iss file '{iss_path}' not found.") return @@ -40,6 +40,6 @@ def run_inno_setup_installer(iss_path: str) -> None: if __name__ == "__main__": build() # Add the ISS Template File - iss_file_path: str = "./EDMC_Installer_Config.iss" + iss_file_path = Path("./EDMC_Installer_Config.iss") # Build the ISS file run_inno_setup_installer(iss_file_path) diff --git a/journal_lock.py b/journal_lock.py index 3e0f8085..3e16b309 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -13,17 +13,13 @@ import tkinter as tk from enum import Enum from os import getpid as os_getpid from tkinter import ttk -from typing import TYPE_CHECKING, Callable - +from typing import Callable +from l10n import translations as tr from config import config from EDMCLogging import get_main_logger logger = get_main_logger() -if TYPE_CHECKING: # pragma: no cover - def _(x: str) -> str: - return x - class JournalLockResult(Enum): """Enumeration of possible outcomes of trying to lock the Journal Directory.""" @@ -212,16 +208,12 @@ class JournalLock: self.parent = parent self.callback = callback # LANG: Title text on popup when Journal directory already locked - self.title(_('Journal directory already locked')) + self.title(tr.tl('Journal directory already locked')) # remove decoration if sys.platform == 'win32': self.attributes('-toolwindow', tk.TRUE) - elif sys.platform == 'darwin': - # http://wiki.tcl.tk/13428 - parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') - self.resizable(tk.FALSE, tk.FALSE) frame = ttk.Frame(self) @@ -229,16 +221,17 @@ class JournalLock: self.blurb = tk.Label(frame) # LANG: Text for when newly selected Journal directory is already locked - self.blurb['text'] = _("The new Journal Directory location is already locked.{CR}" - "You can either attempt to resolve this and then Retry, or choose to Ignore this.") + self.blurb['text'] = tr.tl("The new Journal Directory location is already locked.{CR}" + "You can either attempt to resolve this and then Retry, " + "or choose to Ignore this.") self.blurb.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW) # LANG: Generic 'Retry' button label - self.retry_button = ttk.Button(frame, text=_('Retry'), command=self.retry) + self.retry_button = ttk.Button(frame, text=tr.tl('Retry'), command=self.retry) self.retry_button.grid(row=2, column=0, sticky=tk.EW) # LANG: Generic 'Ignore' button label - self.ignore_button = ttk.Button(frame, text=_('Ignore'), command=self.ignore) + self.ignore_button = ttk.Button(frame, text=tr.tl('Ignore'), command=self.ignore) self.ignore_button.grid(row=2, column=1, sticky=tk.EW) self.protocol("WM_DELETE_WINDOW", self._destroy) diff --git a/killswitch.py b/killswitch.py index 9cc407a6..89a55292 100644 --- a/killswitch.py +++ b/killswitch.py @@ -361,8 +361,8 @@ def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> KillSwitchJSONFile | N logger.warning(f"Failed to get kill switches, data was invalid: {e}") return None - except (requests.exceptions.BaseHTTPError, requests.exceptions.ConnectionError) as e: # type: ignore - logger.warning(f"unable to connect to {target!r}: {e}") + except requests.exceptions.RequestException as requ_err: + logger.warning(f"unable to connect to {target!r}: {requ_err}") return None return data diff --git a/l10n.py b/l10n.py index 26be12fc..6557169e 100755 --- a/l10n.py +++ b/l10n.py @@ -16,17 +16,13 @@ import numbers import re import sys import warnings -from collections import OrderedDict from contextlib import suppress -from os import pardir, listdir, sep, makedirs -from os.path import basename, dirname, isdir, isfile, join, abspath, exists +from os import listdir, sep from typing import TYPE_CHECKING, Iterable, TextIO, cast +import pathlib from config import config from EDMCLogging import get_main_logger -if TYPE_CHECKING: - def _(x: str) -> str: ... - # Note that this is also done in EDMarketConnector.py, and thus removing this here may not have a desired effect try: locale.setlocale(locale.LC_ALL, '') @@ -38,14 +34,9 @@ logger = get_main_logger() # Language name LANGUAGE_ID = '!Language' -LOCALISATION_DIR = 'L10n' +LOCALISATION_DIR: pathlib.Path = pathlib.Path('L10n') -if sys.platform == 'darwin': - from Foundation import ( # type: ignore # exists on Darwin - NSLocale, NSNumberFormatter, NSNumberFormatterDecimalStyle - ) - -elif sys.platform == 'win32': +if sys.platform == 'win32': import ctypes from ctypes.wintypes import BOOL, DWORD, LPCVOID, LPCWSTR, LPWSTR if TYPE_CHECKING: @@ -58,7 +49,6 @@ elif sys.platform == 'win32': GetUserPreferredUILanguages.argtypes = [ DWORD, ctypes.POINTER(ctypes.c_ulong), LPCVOID, ctypes.POINTER(ctypes.c_ulong) ] - GetUserPreferredUILanguages.restype = BOOL LOCALE_NAME_USER_DEFAULT = None @@ -67,7 +57,16 @@ elif sys.platform == 'win32': GetNumberFormatEx.restype = ctypes.c_int -class _Translations: +class Translations: + """ + The Translation System. + + Contains all the logic needed to support multiple languages in EDMC. + DO NOT USE THIS DIRECTLY UNLESS YOU KNOW WHAT YOU'RE DOING. + In most cases, you'll want to import translations. + + For most cases: from l10n import translations as tr. + """ FALLBACK = 'en' # strings in this code are in English FALLBACK_NAME = 'English' @@ -85,6 +84,7 @@ class _Translations: Use when translation is not desired or not available """ self.translations = {None: {}} + # DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later. builtins.__dict__['_'] = lambda x: str(x).replace(r'\"', '"').replace('{CR}', '\n') def install(self, lang: str | None = None) -> None: # noqa: CCR001 @@ -94,7 +94,7 @@ class _Translations: :param lang: The language to translate to, defaults to the preferred language """ available = self.available() - available.add(_Translations.FALLBACK) + available.add(Translations.FALLBACK) if not lang: # Choose the default language for preferred in Locale.preferred_languages(): @@ -117,10 +117,10 @@ class _Translations: self.translations = {None: self.contents(cast(str, lang))} for plugin in listdir(config.plugin_dir_path): - plugin_path = join(config.plugin_dir_path, plugin, LOCALISATION_DIR) - if isdir(plugin_path): + plugin_path = config.plugin_dir_path / plugin / LOCALISATION_DIR + if pathlib.Path.is_dir(plugin_path): try: - self.translations[plugin] = self.contents(cast(str, lang), str(plugin_path)) + self.translations[plugin] = self.contents(cast(str, lang), plugin_path) except UnicodeDecodeError as e: logger.warning(f'Malformed file {lang}.strings in plugin {plugin}: {e}') @@ -128,9 +128,10 @@ class _Translations: except Exception: logger.exception(f'Exception occurred while parsing {lang}.strings in plugin {plugin}') + # DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later. builtins.__dict__['_'] = self.translate - def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]: + def contents(self, lang: str, plugin_path: pathlib.Path | None = None) -> dict[str, str]: """Load all the translations from a translation file.""" assert lang in self.available() translations = {} @@ -141,12 +142,12 @@ class _Translations: for line in h: if line.strip(): - match = _Translations.TRANS_RE.match(line) + match = Translations.TRANS_RE.match(line) if match: to_set = match.group(2).replace(r'\"', '"').replace('{CR}', '\n') translations[match.group(1).replace(r'\"', '"')] = to_set - elif not _Translations.COMMENT_RE.match(line): + elif not Translations.COMMENT_RE.match(line): logger.debug(f'Bad translation: {line.strip()}') h.close() @@ -155,21 +156,45 @@ class _Translations: return translations - def translate(self, x: str, context: str | None = None) -> str: + def tl(self, x: str, context: str | None = None, lang: str | None = None) -> str: + """Use the shorthand Dummy loader for the translate function.""" + return self.translate(x, context, lang) + + def translate(self, x: str, context: str | None = None, lang: str | None = None) -> str: # noqa: CCR001 """ - Translate the given string to the current lang. + Translate the given string to the current lang or an overriden lang. :param x: The string to translate - :param context: Whether or not to search the given directory for translation files, defaults to None + :param context: Contains the full path to the file being localised, from which the plugin name is parsed and + used to locate the plugin translation files, defaults to None + :param lang: Contains a language code to override the EDMC language for this translation, defaults to None :return: The translated string """ + plugin_name: str | None = None + plugin_path: pathlib.Path | None = None + if context: # TODO: There is probably a better way to go about this now. - context = context[len(config.plugin_dir)+1:].split(sep)[0] - if self.translations[None] and context not in self.translations: - logger.debug(f'No translations for {context!r}') + plugin_name = context[len(config.plugin_dir)+1:].split(sep)[0] + plugin_path = config.plugin_dir_path / plugin_name / LOCALISATION_DIR - return self.translations.get(context, {}).get(x) or self.translate(x) + if lang: + contents: dict[str, str] = self.contents(lang=lang, plugin_path=plugin_path) + + if not contents or type(contents) is not dict: + logger.debug(f'Failure loading translations for overridden language {lang!r}') + return self.translate(x) + elif x not in contents.keys(): + logger.debug(f'Missing translation: {x!r} for overridden language {lang!r}') + return self.translate(x) + else: + return contents.get(x) or self.translate(x) + + if plugin_name: + if self.translations[None] and plugin_name not in self.translations: + logger.debug(f'No translations for {plugin_name!r}') + + return self.translations.get(plugin_name, {}).get(x) or self.translate(x) if self.translations[None] and x not in self.translations[None]: logger.debug(f'Missing translation: {x!r}') @@ -179,45 +204,36 @@ class _Translations: def available(self) -> set[str]: """Return a list of available language codes.""" path = self.respath() - if getattr(sys, 'frozen', False) and sys.platform == 'darwin': - available = { - x[:-len('.lproj')] for x in listdir(path) - if x.endswith('.lproj') and isfile(join(x, 'Localizable.strings')) - } - else: - available = {x[:-len('.strings')] for x in listdir(path) if x.endswith('.strings')} + available = {x[:-len('.strings')] for x in listdir(path) if x.endswith('.strings')} return available def available_names(self) -> dict[str | None, str]: """Available language names by code.""" - names: dict[str | None, str] = OrderedDict([ + names: dict[str | None, str] = { # LANG: The system default language choice in Settings > Appearance - (None, _('Default')), # Appearance theme and language setting - ]) + None: self.tl('Default'), # Appearance theme and language setting + } names.update(sorted( [(lang, self.contents(lang).get(LANGUAGE_ID, lang)) for lang in self.available()] + - [(_Translations.FALLBACK, _Translations.FALLBACK_NAME)], + [(Translations.FALLBACK, Translations.FALLBACK_NAME)], key=lambda x: x[1] )) # Sort by name return names - def respath(self) -> str: + def respath(self) -> pathlib.Path: """Path to localisation files.""" if getattr(sys, 'frozen', False): - if sys.platform == 'darwin': - return abspath(join(dirname(sys.executable), pardir, 'Resources')) - - return abspath(join(dirname(sys.executable), LOCALISATION_DIR)) + return pathlib.Path(sys.executable).parent.joinpath(LOCALISATION_DIR).resolve() if __file__: - return abspath(join(dirname(__file__), LOCALISATION_DIR)) + return pathlib.Path(__file__).parent.joinpath(LOCALISATION_DIR).resolve() - return abspath(LOCALISATION_DIR) + return LOCALISATION_DIR.resolve() - def file(self, lang: str, plugin_path: str | None = None) -> TextIO | None: + def file(self, lang: str, plugin_path: pathlib.Path | None = None) -> TextIO | None: """ Open the given lang file for reading. @@ -226,8 +242,8 @@ class _Translations: :return: the opened file (Note: This should be closed when done) """ if plugin_path: - file_path = join(plugin_path, f'{lang}.strings') - if not exists(file_path): + file_path = plugin_path / f"{lang}.strings" + if not file_path.exists(): return None try: @@ -235,36 +251,26 @@ class _Translations: except OSError: logger.exception(f'could not open {file_path}') - elif getattr(sys, 'frozen', False) and sys.platform == 'darwin': - res_path = join(self.respath(), f'{lang}.lproj', 'Localizable.strings') - return open(res_path, encoding='utf-16') - - res_path = join(self.respath(), f'{lang}.strings') + res_path = self.respath() / f'{lang}.strings' return open(res_path, encoding='utf-8') class _Locale: """Locale holds a few utility methods to convert data to and from localized versions.""" - def __init__(self) -> None: - if sys.platform == 'darwin': - self.int_formatter = NSNumberFormatter.alloc().init() - self.int_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle) - self.float_formatter = NSNumberFormatter.alloc().init() - self.float_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle) - self.float_formatter.setMinimumFractionDigits_(5) - self.float_formatter.setMaximumFractionDigits_(5) - + # DEPRECATED: Migrate to _Locale.string_from_number. Will remove in 6.0 or later. def stringFromNumber(self, number: float | int, decimals: int | None = None) -> str: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.string_from_number instead.')) + warnings.warn('use _Locale.string_from_number instead.', DeprecationWarning, stacklevel=2) return self.string_from_number(number, decimals) # type: ignore + # DEPRECATED: Migrate to _Locale.number_from_string. Will remove in 6.0 or later. def numberFromString(self, string: str) -> int | float | None: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.number_from_string instead.')) + warnings.warn('use _Locale.number_from_string instead.', DeprecationWarning, stacklevel=2) return self.number_from_string(string) + # DEPRECATED: Migrate to _Locale.preferred_languages. Will remove in 6.0 or later. def preferredLanguages(self) -> Iterable[str]: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.preferred_languages instead.')) + warnings.warn('use _Locale.preferred_languages instead.', DeprecationWarning, stacklevel=2) return self.preferred_languages() def string_from_number(self, number: float | int, decimals: int = 5) -> str: @@ -280,14 +286,6 @@ class _Locale: if decimals == 0 and not isinstance(number, numbers.Integral): number = int(round(number)) - if sys.platform == 'darwin': - if not decimals and isinstance(number, numbers.Integral): - return self.int_formatter.stringFromNumber_(number) - - self.float_formatter.setMinimumFractionDigits_(decimals) - self.float_formatter.setMaximumFractionDigits_(decimals) - return self.float_formatter.stringFromNumber_(number) - if not decimals and isinstance(number, numbers.Integral): return locale.format_string('%d', number, True) return locale.format_string('%.*f', (decimals, number), True) @@ -300,9 +298,6 @@ class _Locale: :param string: The string to convert :return: None if the string cannot be parsed, otherwise an int or float dependant on input data. """ - if sys.platform == 'darwin': - return self.float_formatter.numberFromString_(string) - with suppress(ValueError): return locale.atoi(string) @@ -333,10 +328,8 @@ class _Locale: :return: The preferred language list """ languages: Iterable[str] - if sys.platform == 'darwin': - languages = NSLocale.preferredLanguages() - elif sys.platform != 'win32': + if sys.platform != 'win32': # POSIX lang = locale.getlocale()[0] languages = [lang.replace('_', '-')] if lang else [] @@ -365,17 +358,32 @@ class _Locale: # singletons Locale = _Locale() -Translations = _Translations() +translations = Translations() +# DEPRECATED: Migrate to `translations`. Will be removed in 6.0 or later. +# 'Translations' singleton is deprecated. +# Begin Deprecation Zone +class _Translations(Translations): + def __init__(self): + warnings.warn('Translations and _Translations() are deprecated. ' + 'Please use translations and Translations() instead.', DeprecationWarning, stacklevel=2) + super().__init__() + + +# Yes, I know this is awful renaming garbage. But we need it for compat. +Translations: Translations = translations # type: ignore +# End Deprecation Zone + # generate template strings file - like xgettext # parsing is limited - only single ' or " delimited strings, and only one string per line if __name__ == "__main__": regexp = re.compile(r'''_\([ur]?(['"])(((? N regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt') oldfiles = sorted([x for x in listdir(config.get_str('outdir')) if regexp.match(x)]) if oldfiles: - with open(join(config.get_str('outdir'), oldfiles[-1]), 'rU') as h: + with open(Path(config.get_str('outdir')) / Path(oldfiles[-1]), 'rU') as h: if h.read() == string: return # same as last time - don't write @@ -53,9 +53,9 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N # Write - output_directory = config.get_str('outdir') + output_directory = Path(config.get_str('outdir')) ship_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) - file_path = join(output_directory, f"{ship}.{ship_time}.txt") + file_path = output_directory / f"{ship}.{ship_time}.txt" with open(file_path, 'wt') as h: h.write(string) diff --git a/resources/modules.json b/modules.json similarity index 89% rename from resources/modules.json rename to modules.json index 27e54626..459ee883 100644 --- a/resources/modules.json +++ b/modules.json @@ -326,30 +326,69 @@ "hpt_antiunknownshutdown_tiny": { "mass": 1.3 }, + "hpt_antiunknownshutdown_tiny_v2": { + "mass": 3 + }, "hpt_atdumbfiremissile_fixed_large": { "mass": 8 }, + "hpt_atdumbfiremissile_fixed_large_v2": { + "mass": 8 + }, "hpt_atdumbfiremissile_fixed_medium": { "mass": 4 }, + "hpt_atdumbfiremissile_fixed_medium_v2": { + "mass": 4 + }, "hpt_atdumbfiremissile_turret_large": { "mass": 8 }, + "hpt_atdumbfiremissile_turret_large_v2": { + "mass": 8 + }, "hpt_atdumbfiremissile_turret_medium": { "mass": 4 }, + "hpt_atdumbfiremissile_turret_medium_v2": { + "mass": 4 + }, "hpt_atmulticannon_fixed_large": { "mass": 8 }, + "hpt_atmulticannon_fixed_large_v2": { + "mass": 8 + }, "hpt_atmulticannon_fixed_medium": { "mass": 4 }, + "hpt_atmulticannon_fixed_medium_v2": { + "mass": 4 + }, + "hpt_atmulticannon_gimbal_large": { + "mass": 8 + }, + "hpt_atmulticannon_gimbal_medium": { + "mass": 4 + }, "hpt_atmulticannon_turret_large": { "mass": 8 }, + "hpt_atmulticannon_turret_large_v2": { + "mass": 8 + }, "hpt_atmulticannon_turret_medium": { "mass": 4 }, + "hpt_atmulticannon_turret_medium_v2": { + "mass": 4 + }, + "hpt_atventdisruptorpylon_fixed_large": { + "mass": 5 + }, + "hpt_atventdisruptorpylon_fixed_medium": { + "mass": 3 + }, "hpt_basicmissilerack_fixed_large": { "mass": 8 }, @@ -416,6 +455,9 @@ "hpt_cannon_gimbal_medium": { "mass": 4 }, + "hpt_cannon_gimbal_medium_burst": { + "mass": 4 + }, "hpt_cannon_gimbal_small": { "mass": 2 }, @@ -446,6 +488,9 @@ "hpt_causticmissile_fixed_medium": { "mass": 4 }, + "hpt_causticsinklauncher_turret_tiny": { + "mass": 1.7 + }, "hpt_chafflauncher_tiny": { "mass": 1.3 }, @@ -608,6 +653,12 @@ "hpt_mininglaser_turret_small": { "mass": 2 }, + "hpt_missing_hardpoint": { + "mass": 0 + }, + "hpt_missing_utility": { + "mass": 0 + }, "hpt_mrascanner_size0_class1": { "mass": 1.3 }, @@ -833,6 +884,9 @@ "hpt_slugshot_turret_small": { "mass": 2 }, + "hpt_xenoscanner_advanced_tiny": { + "mass": 3 + }, "hpt_xenoscanner_basic_tiny": { "mass": 1.3 }, @@ -905,6 +959,12 @@ "int_corrosionproofcargorack_size4_class1": { "mass": 0 }, + "int_corrosionproofcargorack_size5_class1": { + "mass": 0 + }, + "int_corrosionproofcargorack_size6_class1": { + "mass": 0 + }, "int_detailedsurfacescanner_tiny": { "mass": 0 }, @@ -1352,6 +1412,12 @@ "int_engine_size8_class5": { "mass": 160 }, + "int_expmodulestabiliser_size3_class3": { + "mass": 8 + }, + "int_expmodulestabiliser_size5_class3": { + "mass": 20 + }, "int_fighterbay_size5_class1": { "mass": 20 }, @@ -1750,6 +1816,216 @@ "int_hullreinforcement_size5_class2": { "mass": 16 }, + "int_hyperdrive_overcharge_size2_class1": { + "mass": 2.5, + "optmass": 60, + "maxfuel": 0.6, + "fuelmul": 0.008, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class2": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class3": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class4": { + "mass": 2.5, + "optmass": 90, + "maxfuel": 0.9, + "fuelmul": 0.012, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size2_class5": { + "mass": 2.5, + "optmass": 100, + "maxfuel": 1, + "fuelmul": 0.013, + "fuelpower": 2 + }, + "int_hyperdrive_overcharge_size3_class1": { + "mass": 5, + "optmass": 100, + "maxfuel": 1.2, + "fuelmul": 0.008, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class2": { + "mass": 2, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class3": { + "mass": 5, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class4": { + "mass": 5, + "optmass": 150, + "maxfuel": 1.8, + "fuelmul": 0.012, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size3_class5": { + "mass": 5, + "optmass": 167, + "maxfuel": 1.9, + "fuelmul": 0.013, + "fuelpower": 2.15 + }, + "int_hyperdrive_overcharge_size4_class1": { + "mass": 10, + "optmass": 350, + "maxfuel": 2, + "fuelmul": 0.008, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class2": { + "mass": 4, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class3": { + "mass": 10, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class4": { + "mass": 10, + "optmass": 525, + "maxfuel": 3, + "fuelmul": 0.012, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size4_class5": { + "mass": 10, + "optmass": 585, + "maxfuel": 3.2, + "fuelmul": 0.013, + "fuelpower": 2.3 + }, + "int_hyperdrive_overcharge_size5_class1": { + "mass": 20, + "optmass": 700, + "maxfuel": 3.3, + "fuelmul": 0.008, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class2": { + "mass": 8, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class3": { + "mass": 20, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class4": { + "mass": 20, + "optmass": 1050, + "maxfuel": 5, + "fuelmul": 0.012, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size5_class5": { + "mass": 20, + "optmass": 1175, + "maxfuel": 5.2, + "fuelmul": 0.013, + "fuelpower": 2.45 + }, + "int_hyperdrive_overcharge_size6_class1": { + "mass": 40, + "optmass": 1200, + "maxfuel": 5.3, + "fuelmul": 0.008, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class2": { + "mass": 16, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class3": { + "mass": 40, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class4": { + "mass": 40, + "optmass": 1800, + "maxfuel": 8, + "fuelmul": 0.012, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size6_class5": { + "mass": 40, + "optmass": 2000, + "maxfuel": 8.3, + "fuelmul": 0.013, + "fuelpower": 2.6 + }, + "int_hyperdrive_overcharge_size7_class1": { + "mass": 80, + "optmass": 1800, + "maxfuel": 8.5, + "fuelmul": 0.008, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class2": { + "mass": 32, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class3": { + "mass": 80, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class4": { + "mass": 80, + "optmass": 2700, + "maxfuel": 12.8, + "fuelmul": 0.012, + "fuelpower": 2.75 + }, + "int_hyperdrive_overcharge_size7_class5": { + "mass": 80, + "optmass": 3000, + "maxfuel": 13.1, + "fuelmul": 0.013, + "fuelpower": 2.75 + }, "int_hyperdrive_size2_class1": { "mass": 2.5, "optmass": 48, @@ -2145,6 +2421,34 @@ "int_metaalloyhullreinforcement_size5_class2": { "mass": 16 }, + "int_missing_engine": { + "mass": 0 + }, + "int_missing_fueltank": { + "mass": 0 + }, + "int_missing_hyperdrive": { + "mass": 0, + "optmass": 0, + "maxfuel": 0, + "fuelmul": 0, + "fuelpower": 0 + }, + "int_missing_lifesupport": { + "mass": 0 + }, + "int_missing_module": { + "mass": 0 + }, + "int_missing_powerdistributor": { + "mass": 0 + }, + "int_missing_powerplant": { + "mass": 0 + }, + "int_missing_sensors": { + "mass": 0 + }, "int_modulereinforcement_size1_class1": { "mass": 2 }, @@ -2250,6 +2554,9 @@ "int_planetapproachsuite": { "mass": 0 }, + "int_planetapproachsuite_advanced": { + "mass": 0 + }, "int_powerdistributor_size1_class1": { "mass": 1.3 }, @@ -2895,6 +3202,15 @@ "int_shieldcellbank_size8_class5": { "mass": 160 }, + "int_shieldgenerator_size1_class1": { + "mass": 1.3 + }, + "int_shieldgenerator_size1_class2": { + "mass": 0.5 + }, + "int_shieldgenerator_size1_class3": { + "mass": 1.3 + }, "int_shieldgenerator_size1_class3_fast": { "mass": 1.3 }, @@ -3108,6 +3424,21 @@ "mamba_armour_reactive": { "mass": 38 }, + "mandalay_armour_grade1": { + "mass": 0 + }, + "mandalay_armour_grade2": { + "mass": 32 + }, + "mandalay_armour_grade3": { + "mass": 63 + }, + "mandalay_armour_mirrored": { + "mass": 63 + }, + "mandalay_armour_reactive": { + "mass": 63 + }, "orca_armour_grade1": { "mass": 0 }, @@ -3138,6 +3469,21 @@ "python_armour_reactive": { "mass": 53 }, + "python_nx_armour_grade1": { + "mass": 0 + }, + "python_nx_armour_grade2": { + "mass": 26 + }, + "python_nx_armour_grade3": { + "mass": 53 + }, + "python_nx_armour_mirrored": { + "mass": 53 + }, + "python_nx_armour_reactive": { + "mass": 53 + }, "sidewinder_armour_grade1": { "mass": 0 }, @@ -3183,6 +3529,21 @@ "type7_armour_reactive": { "mass": 63 }, + "type8_armour_grade1": { + "mass": 0 + }, + "type8_armour_grade2": { + "mass": 32 + }, + "type8_armour_grade3": { + "mass": 63 + }, + "type8_armour_mirrored": { + "mass": 63 + }, + "type8_armour_reactive": { + "mass": 63 + }, "type9_armour_grade1": { "mass": 0 }, diff --git a/modules.p b/modules.p deleted file mode 100644 index c48f76c4..00000000 Binary files a/modules.p and /dev/null differ diff --git a/monitor.py b/monitor.py index 17a6fa16..e37e77c1 100644 --- a/monitor.py +++ b/monitor.py @@ -14,16 +14,18 @@ import re import sys import threading from calendar import timegm -from collections import OrderedDict, defaultdict +from collections import defaultdict from os import SEEK_END, SEEK_SET, listdir from os.path import basename, expanduser, getctime, isdir, join from time import gmtime, localtime, mktime, sleep, strftime, strptime, time from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping +import psutil import semantic_version import util_ships -from config import config -from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised +from config import config, appname, appversion +from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised, ship_name_map from EDMCLogging import get_main_logger +from edshipyard import ships if TYPE_CHECKING: import tkinter @@ -34,44 +36,17 @@ STARTUP = 'journal.startup' MAX_NAVROUTE_DISCREPANCY = 5 # Timestamp difference in seconds MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds -if TYPE_CHECKING: - def _(x: str) -> str: - return x - -if sys.platform == 'darwin': - from fcntl import fcntl - - from AppKit import NSWorkspace - from watchdog.events import FileSystemEventHandler +if sys.platform == 'win32': + from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver - F_GLOBAL_NOCACHE = 55 - -elif sys.platform == 'win32': - import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR - - from watchdog.events import FileCreatedEvent, FileSystemEventHandler - from watchdog.observers import Observer - from watchdog.observers.api import BaseObserver - - EnumWindows = ctypes.windll.user32.EnumWindows - EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - - CloseHandle = ctypes.windll.kernel32.CloseHandle - - GetWindowText = ctypes.windll.user32.GetWindowTextW - GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - - GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd else: # Linux's inotify doesn't work over CIFS or NFS, so poll FileSystemEventHandler = object # dummy if TYPE_CHECKING: # this isn't ever used, but this will make type checking happy - from watchdog.events import FileCreatedEvent + from watchdog.events import FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver @@ -81,7 +56,8 @@ class EDLogs(FileSystemEventHandler): """Monitoring of Journal files.""" # Magic with FileSystemEventHandler can confuse type checkers when they do not have access to every import - _POLL = 1 # Polling is cheap, so do it often + _POLL = 1 # Polling while running is cheap, so do it often + _INACTIVE_POLL = 10 # Polling while not running isn't as cheap, so do it less often _RE_CANONICALISE = re.compile(r'\$(.+)_name;') _RE_CATEGORY = re.compile(r'\$MICRORESOURCE_CATEGORY_(.+);') _RE_LOGFILE = re.compile(r'^Journal(Alpha|Beta)?\.[0-9]{2,4}(-)?[0-9]{2}(-)?[0-9]{2}(T)?[0-9]{2}[0-9]{2}[0-9]{2}' @@ -111,6 +87,7 @@ class EDLogs(FileSystemEventHandler): self.catching_up = False self.game_was_running = False # For generation of the "ShutDown" event + self.running_process = None # Context for journal handling self.version: str | None = None @@ -120,6 +97,7 @@ class EDLogs(FileSystemEventHandler): self.group: str | None = None self.cmdr: str | None = None self.started: int | None = None # Timestamp of the LoadGame event + self.slef: str | None = None self._navroute_retries_remaining = 0 self._last_navroute_journal_timestamp: float | None = None @@ -163,6 +141,10 @@ class EDLogs(FileSystemEventHandler): 'ShipType': None, 'HullValue': None, 'ModulesValue': None, + 'UnladenMass': None, + 'CargoCapacity': None, + 'MaxJumpRange': None, + 'FuelCapacity': None, 'Rebuy': None, 'Modules': None, 'CargoJSON': None, # The raw data from the last time cargo.json was read @@ -197,6 +179,13 @@ class EDLogs(FileSystemEventHandler): 'StationName': None, 'NavRoute': None, + 'Powerplay': { + 'Power': None, + 'Rank': None, + 'Merits': None, + 'Votes': None, + 'TimePledged': None, + }, } def start(self, root: 'tkinter.Tk') -> bool: # noqa: CCR001 @@ -357,11 +346,11 @@ class EDLogs(FileSystemEventHandler): """ return bool(self.thread and self.thread.is_alive()) - def on_created(self, event: 'FileCreatedEvent') -> None: + def on_created(self, event: 'FileSystemEvent') -> None: """Watchdog callback when, e.g. client (re)started.""" - if not event.is_directory and self._RE_LOGFILE.search(basename(event.src_path)): + if not event.is_directory and self._RE_LOGFILE.search(str(basename(event.src_path))): - self.logfile = event.src_path + self.logfile = event.src_path # type: ignore def worker(self) -> None: # noqa: C901, CCR001 """ @@ -382,8 +371,6 @@ class EDLogs(FileSystemEventHandler): logfile = self.logfile if logfile: loghandle: BinaryIO = open(logfile, 'rb', 0) # unbuffered - if sys.platform == 'darwin': - fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB self.catching_up = True for line in loghandle: @@ -450,7 +437,7 @@ class EDLogs(FileSystemEventHandler): new_journal_file = None if logfile: - loghandle.seek(0, SEEK_END) # required to make macOS notice log change over SMB + loghandle.seek(0, SEEK_END) # required for macOS to notice log change over SMB. TODO: Do we need this? loghandle.seek(log_pos, SEEK_SET) # reset EOF flag # TODO: log_pos reported as possibly unbound for line in loghandle: # Paranoia check to see if we're shutting down @@ -483,12 +470,12 @@ class EDLogs(FileSystemEventHandler): if logfile: loghandle = open(logfile, 'rb', 0) # unbuffered - if sys.platform == 'darwin': - fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB - log_pos = 0 - sleep(self._POLL) + if self.game_was_running: + sleep(self._POLL) + else: + sleep(self._INACTIVE_POLL) # Check whether we're still supposed to be running if threading.current_thread() != self.thread: @@ -567,7 +554,7 @@ class EDLogs(FileSystemEventHandler): try: # Preserve property order because why not? - entry: MutableMapping[str, Any] = json.loads(line, object_pairs_hook=OrderedDict) + entry: MutableMapping[str, Any] = json.loads(line) assert 'timestamp' in entry, "Timestamp does not exist in the entry" self.__navroute_retry() @@ -704,6 +691,11 @@ class EDLogs(FileSystemEventHandler): self.state['ShipType'] = self.canonicalise(entry['Ship']) self.state['HullValue'] = entry.get('HullValue') # not present on exiting Outfitting self.state['ModulesValue'] = entry.get('ModulesValue') # not present on exiting Outfitting + self.state['UnladenMass'] = entry.get('UnladenMass') + self.state['CargoCapacity'] = entry.get('CargoCapacity') + self.state['MaxJumpRange'] = entry.get('MaxJumpRange') + self.state["FuelCapacity"] = {name: entry.get("FuelCapacity", {}).get(name) for name in + ("Main", "Reserve")} self.state['Rebuy'] = entry.get('Rebuy') # Remove spurious differences between initial Loadout event and subsequent self.state['Modules'] = {} @@ -717,6 +709,34 @@ class EDLogs(FileSystemEventHandler): module.pop('AmmoInHopper') self.state['Modules'][module['Slot']] = module + # SLEF + initial_dict: dict[str, dict[str, Any]] = { + "header": {"appName": appname, "appVersion": str(appversion())} + } + data_dict = {} + for module in entry['Modules']: + if module.get('Slot') == 'FuelTank': + cap = module['Item'].split('size') + cap = cap[1].split('_') + cap = 2 ** int(cap[0]) + ship = ship_name_map[entry["Ship"]] + fuel = {'Main': cap, 'Reserve': ships[ship]['reserveFuelCapacity']} + data_dict.update({"FuelCapacity": fuel}) + data_dict.update({ + 'Ship': entry["Ship"], + 'ShipName': entry['ShipName'], + 'ShipIdent': entry['ShipIdent'], + 'HullValue': entry.get('HullValue'), # type: ignore + 'ModulesValue': entry.get('ModulesValue'), # type: ignore + 'Rebuy': entry['Rebuy'], + 'MaxJumpRange': entry['MaxJumpRange'], + 'UnladenMass': entry['UnladenMass'], + 'CargoCapacity': entry['CargoCapacity'], + 'Modules': entry['Modules'], + }) + initial_dict.update({'data': data_dict}) + output = json.dumps(initial_dict, indent=4) + self.slef = str(f"[{output}]") elif event_type == 'modulebuy': self.state['Modules'][entry['Slot']] = { @@ -1042,7 +1062,7 @@ class EDLogs(FileSystemEventHandler): rank[k] = (rank[k][0], min(v, 100)) elif event_type in ('reputation', 'statistics'): - payload = OrderedDict(entry) + payload = dict(entry) payload.pop('event') payload.pop('timestamp') # NB: We need the original casing for these keys @@ -1073,7 +1093,7 @@ class EDLogs(FileSystemEventHandler): # From 3.3 full Cargo event (after the first one) is written to a separate file if 'Inventory' not in entry: with open(join(self.currentdir, 'Cargo.json'), 'rb') as h: # type: ignore - entry = json.load(h, object_pairs_hook=OrderedDict) # Preserve property order because why not? + entry = json.load(h) self.state['CargoJSON'] = entry clean = self.coalesce_cargo(entry['Inventory']) @@ -1108,7 +1128,7 @@ class EDLogs(FileSystemEventHandler): attempts += 1 try: with open(shiplocker_filename, 'rb') as h: - entry = json.load(h, object_pairs_hook=OrderedDict) + entry = json.load(h) self.state['ShipLockerJSON'] = entry break @@ -1826,6 +1846,13 @@ class EDLogs(FileSystemEventHandler): # There should be a `Backpack` event as you 'come to' in the # new location, so no need to zero out BackPack here. + elif event_type == 'powerplay': + self.state['Powerplay']['Power'] = entry.get('Power', '') + self.state['Powerplay']['Rank'] = entry.get('Rank', 0) + self.state['Powerplay']['Merits'] = entry.get('Merits', 0) + self.state['Powerplay']['Votes'] = entry.get('Votes', 0) + self.state['Powerplay']['TimePledged'] = entry.get('TimePledged', 0) + return entry except Exception as ex: @@ -2136,41 +2163,33 @@ class EDLogs(FileSystemEventHandler): return entry - def game_running(self) -> bool: # noqa: CCR001 + def game_running(self) -> bool: """ Determine if the game is currently running. - TODO: Implement on Linux - :return: bool - True if the game is running. """ - if sys.platform == 'darwin': - for app in NSWorkspace.sharedWorkspace().runningApplications(): - if app.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': - return True - - elif sys.platform == 'win32': - def WindowTitle(h): # noqa: N802 - if h: - length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(length) - if GetWindowText(h, buf, length): - return buf.value - return None - - def callback(hWnd, lParam): # noqa: N803 - name = WindowTitle(hWnd) - if name and name.startswith('Elite - Dangerous'): - handle = GetProcessHandleFromHwnd(hWnd) - if handle: # If GetProcessHandleFromHwnd succeeds then the app is already running as this user - CloseHandle(handle) - return False # stop enumeration - - return True - - return not EnumWindows(EnumWindowsProc(callback), 0) - - return False + if self.running_process: + p = self.running_process + try: + with p.oneshot(): + if p.status() not in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]: + raise psutil.NoSuchProcess(p.pid) + except psutil.NoSuchProcess: + # Process likely expired + self.running_process = None + if not self.running_process: + try: + edmc_process = psutil.Process() + edmc_user = edmc_process.username() + for proc in psutil.process_iter(['name', 'username']): + if 'EliteDangerous' in proc.info['name'] and proc.info['username'] == edmc_user: + self.running_process = proc + return True + except psutil.NoSuchProcess: + pass + return False + return bool(self.running_process) def ship(self, timestamped=True) -> MutableMapping[str, Any] | None: """ @@ -2189,7 +2208,7 @@ class EDLogs(FileSystemEventHandler): 'PowerDistributor', 'Radar', 'FuelTank' ) - d: MutableMapping[str, Any] = OrderedDict() + d: MutableMapping[str, Any] = {} if timestamped: d['timestamp'] = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()) diff --git a/myNotebook.py b/myNotebook.py index 6fa35774..c57a6deb 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -1,12 +1,9 @@ """ Custom `ttk.Notebook` to fix various display issues. -Hacks to fix various display issues with notebooks and their child widgets on -OSX and Windows. +Hacks to fix various display issues with notebooks and their child widgets on Windows. - Windows: page background should be White, not SystemButtonFace -- OSX: page background should be a darker gray than systemWindowBody - selected tab foreground should be White when the window is active Entire file may be imported by plugins. """ @@ -14,15 +11,12 @@ from __future__ import annotations import sys import tkinter as tk -from tkinter import ttk +import warnings +from tkinter import ttk, messagebox +from PIL import ImageGrab +from l10n import translations as tr -# Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult -if sys.platform == 'darwin': - from platform import mac_ver - PAGEFG = 'systemButtonText' - PAGEBG = 'systemButtonActiveDarkShadow' - -elif sys.platform == 'win32': +if sys.platform == 'win32': PAGEFG = 'SystemWindowText' PAGEBG = 'SystemWindow' # typically white @@ -32,39 +26,22 @@ class Notebook(ttk.Notebook): def __init__(self, master: ttk.Frame | None = None, **kw): - ttk.Notebook.__init__(self, master, **kw) + super().__init__(master, **kw) style = ttk.Style() - - if sys.platform == 'darwin': - if list(map(int, mac_ver()[0].split('.'))) >= [10, 10]: - # Hack for tab appearance with 8.5 on Yosemite & El Capitan. For proper fix see - # https://github.com/tcltk/tk/commit/55c4dfca9353bbd69bbcec5d63bf1c8dfb461e25 - style.configure('TNotebook.Tab', padding=(12, 10, 12, 2)) - style.map('TNotebook.Tab', foreground=[('selected', '!background', 'systemWhite')]) - self.grid(sticky=tk.NSEW) # Already padded apropriately - elif sys.platform == 'win32': + if sys.platform == 'win32': style.configure('nb.TFrame', background=PAGEBG) style.configure('nb.TButton', background=PAGEBG) style.configure('nb.TCheckbutton', foreground=PAGEFG, background=PAGEBG) style.configure('nb.TMenubutton', foreground=PAGEFG, background=PAGEBG) style.configure('nb.TRadiobutton', foreground=PAGEFG, background=PAGEBG) - self.grid(padx=10, pady=10, sticky=tk.NSEW) - else: - self.grid(padx=10, pady=10, sticky=tk.NSEW) + self.grid(padx=10, pady=10, sticky=tk.NSEW) -# FIXME: The real fix for this 'dynamic type' would be to split this whole -# thing into being a module with per-platform files, as we've done with config -# That would also make the code cleaner. -class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): # type: ignore - """Custom t(t)k.Frame class to fix some display issues.""" +class Frame(ttk.Frame): + """Custom ttk.Frame class to fix some display issues.""" def __init__(self, master: ttk.Notebook | None = None, **kw): - if sys.platform == 'darwin': - kw['background'] = kw.pop('background', PAGEBG) - tk.Frame.__init__(self, master, **kw) - tk.Frame(self).grid(pady=5) - elif sys.platform == 'win32': + if sys.platform == 'win32': ttk.Frame.__init__(self, master, style='nb.TFrame', **kw) ttk.Frame(self).grid(pady=5) # top spacer else: @@ -77,105 +54,130 @@ class Label(tk.Label): """Custom tk.Label class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - # This format chosen over `sys.platform in (...)` as mypy and friends dont understand that - if sys.platform in ('darwin', 'win32'): - kw['foreground'] = kw.pop('foreground', PAGEFG) - kw['background'] = kw.pop('background', PAGEBG) - else: - kw['foreground'] = kw.pop('foreground', ttk.Style().lookup('TLabel', 'foreground')) - kw['background'] = kw.pop('background', ttk.Style().lookup('TLabel', 'background')) - tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms + kw['foreground'] = kw.pop('foreground', PAGEFG if sys.platform == 'win32' + else ttk.Style().lookup('TLabel', 'foreground')) + kw['background'] = kw.pop('background', PAGEBG if sys.platform == 'win32' + else ttk.Style().lookup('TLabel', 'background')) + super().__init__(master, **kw) -class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): # type: ignore - """Custom t(t)k.Entry class to fix some display issues.""" +class EntryMenu(ttk.Entry): + """Extended entry widget that includes a context menu with Copy, Cut-and-Paste commands.""" + + def __init__(self, *args, **kwargs) -> None: + ttk.Entry.__init__(self, *args, **kwargs) + + self.menu = tk.Menu(self, tearoff=False) + self.menu.add_command(label=tr.tl("Copy"), command=self.copy) # LANG: Label for 'Copy' as in 'Copy and Paste' + self.menu.add_command(label=tr.tl("Cut"), command=self.cut) # LANG: Label for 'Cut' as in 'Cut and Paste' + self.menu.add_separator() + # LANG: Label for 'Paste' as in 'Copy and Paste' + self.menu.add_command(label=tr.tl("Paste"), command=self.paste) + self.menu.add_separator() + # LANG: Label for 'Select All' + self.menu.add_command(label=tr.tl("Select All"), command=self.select_all) + + self.bind("", self.display_popup) + + def display_popup(self, event: tk.Event) -> None: + """Display the menu popup.""" + self.menu.post(event.x_root, event.y_root) + + def select_all(self) -> None: + """Select all the text within the Entry.""" + self.selection_range(0, tk.END) + self.focus_set() + + def copy(self) -> None: + """Copy the selected Entry text.""" + if self.selection_present(): + self.clipboard_clear() + self.clipboard_append(self.selection_get()) + + def cut(self) -> None: + """Cut the selected Entry text.""" + if self.selection_present(): + self.copy() + self.delete(tk.SEL_FIRST, tk.SEL_LAST) + + def paste(self) -> None: + """Paste the selected Entry text.""" + try: + # Attempt to grab an image from the clipboard (apprently also works for files) + img = ImageGrab.grabclipboard() + if img: + # Hijack existing translation, yes it doesn't exactly match here. + messagebox.showwarning( + tr.tl('Error'), # LANG: Generic error prefix - following text is from Frontier auth service; + tr.tl('Cannot paste non-text content.'), # LANG: Can't Paste Images or Files in Text + parent=self.master + ) + return + text = self.clipboard_get() + if self.selection_present() and text: + self.delete(tk.SEL_FIRST, tk.SEL_LAST) + self.insert(tk.INSERT, text) + except tk.TclError: + # No text in clipboard or clipboard is not text + pass + + +class Entry(EntryMenu): + """Custom ttk.Entry class to fix some display issues.""" + + # DEPRECATED: Migrate to EntryMenu. Will remove in 6.0 or later. + def __init__(self, master: ttk.Frame | None = None, **kw): + warnings.warn('Migrate to EntryMenu. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) + EntryMenu.__init__(self, master, **kw) + + +class Button(ttk.Button): + """Custom ttk.Button class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - if sys.platform == 'darwin': - kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG) - tk.Entry.__init__(self, master, **kw) - else: - ttk.Entry.__init__(self, master, **kw) - - -class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): # type: ignore - """Custom t(t)k.Button class to fix some display issues.""" - - def __init__(self, master: ttk.Frame | None = None, **kw): - if sys.platform == 'darwin': - kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG) - tk.Button.__init__(self, master, **kw) - elif sys.platform == 'win32': + if sys.platform == 'win32': ttk.Button.__init__(self, master, style='nb.TButton', **kw) else: ttk.Button.__init__(self, master, **kw) -class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): # type: ignore - """Custom t(t)k.ColoredButton class to fix some display issues.""" +class ColoredButton(tk.Button): + """Custom tk.Button class to fix some display issues.""" + + # DEPRECATED: Migrate to tk.Button. Will remove in 6.0 or later. + def __init__(self, master: ttk.Frame | None = None, **kw): + warnings.warn('Migrate to tk.Button. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) + tk.Button.__init__(self, master, **kw) + + +class Checkbutton(ttk.Checkbutton): + """Custom ttk.Checkbutton class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - if sys.platform == 'darwin': - # Can't set Button background on OSX, so use a Label instead - kw['relief'] = kw.pop('relief', tk.RAISED) - self._command = kw.pop('command', None) - tk.Label.__init__(self, master, **kw) - self.bind('', self._press) - else: - tk.Button.__init__(self, master, **kw) - - if sys.platform == 'darwin': - def _press(self, event): - self._command() + style = 'nb.TCheckbutton' if sys.platform == 'win32' else None + super().__init__(master, style=style, **kw) # type: ignore -class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): # type: ignore - """Custom t(t)k.Checkbutton class to fix some display issues.""" +class Radiobutton(ttk.Radiobutton): + """Custom ttk.Radiobutton class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - if sys.platform == 'darwin': - kw['foreground'] = kw.pop('foreground', PAGEFG) - kw['background'] = kw.pop('background', PAGEBG) - tk.Checkbutton.__init__(self, master, **kw) - elif sys.platform == 'win32': - ttk.Checkbutton.__init__(self, master, style='nb.TCheckbutton', **kw) - else: - ttk.Checkbutton.__init__(self, master, **kw) + style = 'nb.TRadiobutton' if sys.platform == 'win32' else None + super().__init__(master, style=style, **kw) # type: ignore -class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton): # type: ignore - """Custom t(t)k.Radiobutton class to fix some display issues.""" - - def __init__(self, master: ttk.Frame | None = None, **kw): - if sys.platform == 'darwin': - kw['foreground'] = kw.pop('foreground', PAGEFG) - kw['background'] = kw.pop('background', PAGEBG) - tk.Radiobutton.__init__(self, master, **kw) - elif sys.platform == 'win32': - ttk.Radiobutton.__init__(self, master, style='nb.TRadiobutton', **kw) - else: - ttk.Radiobutton.__init__(self, master, **kw) - - -class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu): # type: ignore - """Custom t(t)k.OptionMenu class to fix some display issues.""" +class OptionMenu(ttk.OptionMenu): + """Custom ttk.OptionMenu class to fix some display issues.""" def __init__(self, master, variable, default=None, *values, **kw): - if sys.platform == 'darwin': - variable.set(default) - bg = kw.pop('background', PAGEBG) - tk.OptionMenu.__init__(self, master, variable, *values, **kw) - self['background'] = bg - elif sys.platform == 'win32': + if sys.platform == 'win32': # OptionMenu derives from Menubutton at the Python level, so uses Menubutton's style ttk.OptionMenu.__init__(self, master, variable, default, *values, style='nb.TMenubutton', **kw) self['menu'].configure(background=PAGEBG) - # Workaround for https://bugs.python.org/issue25684 - for i in range(0, self['menu'].index('end')+1): - self['menu'].entryconfig(i, variable=variable) else: ttk.OptionMenu.__init__(self, master, variable, default, *values, **kw) self['menu'].configure(background=ttk.Style().lookup('TMenu', 'background')) - # Workaround for https://bugs.python.org/issue25684 - for i in range(0, self['menu'].index('end')+1): - self['menu'].entryconfig(i, variable=variable) + + # Workaround for https://bugs.python.org/issue25684 + for i in range(0, self['menu'].index('end') + 1): + self['menu'].entryconfig(i, variable=variable) diff --git a/outfitting.py b/outfitting.py index 07e8ae76..770c47cd 100644 --- a/outfitting.py +++ b/outfitting.py @@ -8,8 +8,6 @@ See LICENSE file. from __future__ import annotations import json -from collections import OrderedDict -from typing import OrderedDict as OrderedDictT from config import config from edmc_data import ( outfitting_armour_map as armour_map, @@ -36,7 +34,7 @@ from EDMCLogging import get_main_logger logger = get_main_logger() # Module mass, FSD data etc -moduledata: OrderedDictT = OrderedDict() +moduledata: dict = {} def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR001 @@ -57,7 +55,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 """ # Lazily populate if not moduledata: - modules_path = config.respath_path / "resources" / "modules.json" + modules_path = config.respath_path / "modules.json" moduledata.update(json.loads(modules_path.read_text())) if not module.get('name'): @@ -69,7 +67,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 # Armour - e.g. Federation_Dropship_Armour_Grade2 if name[-2] == 'armour': # Armour is ship-specific, and ship names can have underscores - ship_name, armour_grade = module["name"].lower().rsplit("_", 2)[0:2] + ship_name, armour, armour_grade = module["name"].lower().rsplit("_", 2)[0:3] if ship_name not in ship_map: raise AssertionError(f"Unknown ship: {ship_name}") new['category'] = 'standard' @@ -79,7 +77,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 new['rating'] = 'I' # Skip uninteresting stuff - some no longer present in ED 3.1 cAPI data - elif (name[0] in [ + elif (name[0] in ( 'bobble', 'decal', 'nameplate', @@ -87,7 +85,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 'enginecustomisation', 'voicepack', 'weaponcustomisation' - ] + ) or name[1].startswith('shipkit')): return None @@ -100,6 +98,12 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 elif not entitled and name[1] == 'planetapproachsuite': return None + # V2 Shutdown Field Neutralizer - Hpt_AntiUnknownShutdown_Tiny_V2 + elif name[0] == 'hpt' and name[1] in countermeasure_map and len(name) == 4 and name[3] == 'v2': + new['category'] = 'utility' + new['name'], new['rating'] = countermeasure_map[name[1]] + new['class'] = weaponclass_map[name[-2]] + # Countermeasures - e.g. Hpt_PlasmaPointDefence_Turret_Tiny elif name[0] == 'hpt' and name[1] in countermeasure_map: new['category'] = 'utility' @@ -181,12 +185,18 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 if name[1] == 'dronecontrol': # e.g. Int_DroneControl_Collection_Size1_Class1 name.pop(0) + elif name[1] == 'multidronecontrol': # e.g. Int_MultiDroneControl_Rescue_Size3_Class3 + name.pop(0) + elif name[-1] == 'free': # Starter Sidewinder or Freagle modules - just treat them like vanilla modules name.pop() if name[1] in standard_map: # e.g. Int_Engine_Size2_Class1, Int_ShieldGenerator_Size8_Class5_Strong new['category'] = 'standard' - new['name'] = standard_map[len(name) > 4 and (name[1], name[4]) or name[1]] + if name[2] == 'overcharge': + new['name'] = standard_map[(name[1], name[2])] + else: + new['name'] = standard_map[len(name) > 4 and (name[1], name[4]) or name[1]] elif name[1] in internal_map: # e.g. Int_CargoRack_Size8_Class1 new['category'] = 'internal' @@ -205,12 +215,15 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 elif len(name) < 4 and name[1] == 'resourcesiphon': # Hack! 128066402 has no size or class. (new['class'], new['rating']) = ('1', 'I') - elif len(name) < 4 and name[1] in ['guardianpowerdistributor', 'guardianpowerplant']: # Hack! No class. + elif len(name) < 4 and name[1] in ('guardianpowerdistributor', 'guardianpowerplant'): # Hack! No class. (new['class'], new['rating']) = (str(name[2][4:]), 'A') - elif len(name) < 4 and name[1] in ['guardianfsdbooster']: # Hack! No class. + elif len(name) < 4 and name[1] == 'guardianfsdbooster': # Hack! No class. (new['class'], new['rating']) = (str(name[2][4:]), 'H') + elif len(name) > 4 and name[1] == 'hyperdrive': # e.g. Int_Hyperdrive_Overcharge_Size6_Class3 + (new['class'], new['rating']) = (str(name[3][-1:]), rating_map[name[4][-1:]]) + else: if len(name) < 3: raise AssertionError(f'{name}: length < 3]') @@ -244,7 +257,7 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0 if not m: print(f'No data for module {key}') - elif new['name'] == 'Frame Shift Drive': + elif new['name'] == 'Frame Shift Drive' or new['name'] == 'Frame Shift Drive (SCO)': assert 'mass' in m and 'optmass' in m and 'maxfuel' in m and 'fuelmul' in m and 'fuelpower' in m, m else: diff --git a/plug.py b/plug.py index aa34ce77..ee53e76b 100644 --- a/plug.py +++ b/plug.py @@ -14,6 +14,7 @@ import operator import os import sys import tkinter as tk +from pathlib import Path from tkinter import ttk from typing import Any, Mapping, MutableMapping @@ -47,14 +48,14 @@ last_error = LastError() class Plugin: """An EDMC plugin.""" - def __init__(self, name: str, loadfile: str | None, plugin_logger: logging.Logger | None): + def __init__(self, name: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001 """ Load a single plugin. :param name: Base name of the file being loaded from. :param loadfile: Full path/filename of the plugin. :param plugin_logger: The logging instance for this plugin to use. - :raises Exception: Typically ImportError or OSError + :raises Exception: Typically, ImportError or OSError """ self.name: str = name # Display name. self.folder: str | None = name # basename of plugin folder. None for internal plugins. @@ -66,19 +67,23 @@ class Plugin: try: filename = 'plugin_' filename += name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_') - module = importlib.machinery.SourceFileLoader( - filename, - loadfile - ).load_module() - if getattr(module, 'plugin_start3', None): - newname = module.plugin_start3(os.path.dirname(loadfile)) - self.name = str(newname) if newname else self.name - self.module = module - elif getattr(module, 'plugin_start', None): - logger.warning(f'plugin {name} needs migrating\n') - PLUGINS_not_py3.append(self) + spec = importlib.util.spec_from_file_location(filename, loadfile) + # Replaces older load_module() code. Includes a safety check that the module name is set. + if spec is not None and spec.loader is not None: + module = importlib.util.module_from_spec(spec) + sys.modules[module.__name__] = module + spec.loader.exec_module(module) + if getattr(module, 'plugin_start3', None): + newname = module.plugin_start3(Path(loadfile).resolve().parent) + self.name = str(newname) if newname else self.name + self.module = module + elif getattr(module, 'plugin_start', None): + logger.warning(f'plugin {name} needs migrating\n') + PLUGINS_not_py3.append(self) + else: + logger.error(f'plugin {name} has no plugin_start3() function') else: - logger.error(f'plugin {name} has no plugin_start3() function') + logger.error(f'Failed to load Plugin "{name}" from file "{loadfile}"') except Exception: logger.exception(f': Failed for Plugin "{name}"') raise @@ -126,7 +131,7 @@ class Plugin: return None - def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame | None: + def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame | None: """ If the plugin provides a prefs frame, create and return it. @@ -165,9 +170,11 @@ def load_plugins(master: tk.Tk) -> None: def _load_internal_plugins(): internal = [] for name in sorted(os.listdir(config.internal_plugin_dir_path)): - if name.endswith('.py') and name[0] not in ['.', '_']: + if name.endswith('.py') and name[0] not in ('.', '_'): try: - plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir_path, name), logger) + plugin_name = name[:-3] + plugin_path = config.internal_plugin_dir_path / name + plugin = Plugin(plugin_name, plugin_path, logger) plugin.folder = None internal.append(plugin) except Exception: @@ -182,9 +189,12 @@ def _load_found_plugins(): # The intent here is to e.g. have EDMC-Overlay load before any plugins # that depend on it. - for name in sorted(os.listdir(config.plugin_dir_path), key=lambda n: ( - not os.path.isfile(os.path.join(config.plugin_dir_path, n, '__init__.py')), n.lower())): - if not os.path.isdir(os.path.join(config.plugin_dir_path, name)) or name[0] in ['.', '_']: + plugin_files = sorted(config.plugin_dir_path.iterdir(), key=lambda p: ( + not (p / '__init__.py').is_file(), p.name.lower())) + + for plugin_file in plugin_files: + name = plugin_file.name + if not (config.plugin_dir_path / name).is_dir() or name.startswith(('.', '_')): pass elif name.endswith('.disabled'): name, discard = name.rsplit('.', 1) @@ -192,12 +202,12 @@ def _load_found_plugins(): else: try: # Add plugin's folder to load path in case plugin has internal package dependencies - sys.path.append(os.path.join(config.plugin_dir_path, name)) + sys.path.append(str(config.plugin_dir_path / name)) import EDMCLogging # Create a logger for this 'found' plugin. Must be before the load.py is loaded. plugin_logger = EDMCLogging.get_plugin_logger(name) - found.append(Plugin(name, os.path.join(config.plugin_dir_path, name, 'load.py'), plugin_logger)) + found.append(Plugin(name, config.plugin_dir_path / name / 'load.py', plugin_logger)) except Exception: PLUGINS_broken.append(Plugin(name, None, logger)) logger.exception(f'Failure loading found Plugin "{name}"') @@ -414,7 +424,7 @@ def notify_capidata(data: companion.CAPIData, is_beta: bool) -> str | None: def notify_capi_fleetcarrierdata(data: companion.CAPIData) -> str | None: """ - Send the latest CAPI Fleetcarrier data from the FD servers to each plugin. + Send the latest CAPI Fleet Carrier data from the FD servers to each plugin. :param data: The CAPIData returned in the CAPI response :returns: Error message from the first plugin that returns one (if any) @@ -429,7 +439,7 @@ def notify_capi_fleetcarrierdata(data: companion.CAPIData) -> str | None: error = error if error else newerror except Exception: - logger.exception(f'Plugin "{plugin.name}" failed on receiving Fleetcarrier data') + logger.exception(f'Plugin "{plugin.name}" failed on receiving Fleet Carrier data') return error diff --git a/plugins/coriolis.py b/plugins/coriolis.py index 9de635d0..a9eabced 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -27,15 +27,11 @@ import io import json import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING import myNotebook as nb # noqa: N813 # its not my fault. from EDMCLogging import get_main_logger from plug import show_error from config import config - -if TYPE_CHECKING: - def _(s: str) -> str: - ... +from l10n import translations as tr class CoriolisConfig: @@ -45,12 +41,15 @@ class CoriolisConfig: self.normal_url = '' self.beta_url = '' self.override_mode = '' + self.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto + self.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal + self.override_text_old_beta = tr.tl('Beta') # LANG: Coriolis normal/beta selection - beta self.normal_textvar = tk.StringVar() self.beta_textvar = tk.StringVar() self.override_textvar = tk.StringVar() - def initialize_urls(self): + def initialize_urls(self) -> None: """Initialize Coriolis URLs and override mode from configuration.""" self.normal_url = config.get_str('coriolis_normal_url', default=DEFAULT_NORMAL_URL) self.beta_url = config.get_str('coriolis_beta_url', default=DEFAULT_BETA_URL) @@ -60,10 +59,10 @@ class CoriolisConfig: self.beta_textvar.set(value=self.beta_url) self.override_textvar.set( value={ - 'auto': _('Auto'), # LANG: 'Auto' label for Coriolis site override selection - 'normal': _('Normal'), # LANG: 'Normal' label for Coriolis site override selection - 'beta': _('Beta') # LANG: 'Beta' label for Coriolis site override selection - }.get(self.override_mode, _('Auto')) # LANG: 'Auto' label for Coriolis site override selection + 'auto': tr.tl('Auto'), # LANG: 'Auto' label for Coriolis site override selection + 'normal': tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + 'beta': tr.tl('Beta') # LANG: 'Beta' label for Coriolis site override selection + }.get(self.override_mode, tr.tl('Auto')) # LANG: 'Auto' label for Coriolis site override selection ) @@ -81,41 +80,45 @@ def plugin_start3(path: str) -> str: return 'Coriolis' -def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: """Set up plugin preferences.""" PADX = 10 # noqa: N806 PADY = 1 # noqa: N806 BOXY = 2 # noqa: N806 # box spacing + # Save the old text values for the override mode, so we can update them if the language is changed + coriolis_config.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto + coriolis_config.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal + coriolis_config.override_text_old_beta = tr.tl('Beta') # LANG: Coriolis normal/beta selection - beta + conf_frame = nb.Frame(parent) conf_frame.columnconfigure(index=1, weight=1) cur_row = 0 # LANG: Settings>Coriolis: Help/hint for changing coriolis URLs - nb.Label(conf_frame, text=_( + nb.Label(conf_frame, text=tr.tl( "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" )).grid(sticky=tk.EW, row=cur_row, column=0, padx=PADX, pady=PADY, columnspan=3) cur_row += 1 # LANG: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL - nb.Label(conf_frame, text=_('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) - nb.Entry(conf_frame, - textvariable=coriolis_config.normal_textvar).grid( + nb.Label(conf_frame, text=tr.tl('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + nb.EntryMenu(conf_frame, textvariable=coriolis_config.normal_textvar).grid( sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY ) # LANG: Generic 'Reset' button label - nb.Button(conf_frame, text=_("Reset"), + nb.Button(conf_frame, text=tr.tl("Reset"), command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) cur_row += 1 # LANG: Settings>Coriolis: Label for 'alpha/beta game version' URL - nb.Label(conf_frame, text=_('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) - nb.Entry(conf_frame, textvariable=coriolis_config.beta_textvar).grid( - sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY + nb.Label(conf_frame, text=tr.tl('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + nb.EntryMenu(conf_frame, textvariable=coriolis_config.beta_textvar).grid( + sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY ) # LANG: Generic 'Reset' button label - nb.Button(conf_frame, text=_('Reset'), + nb.Button(conf_frame, text=tr.tl('Reset'), command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) @@ -123,16 +126,16 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr # TODO: This needs a help/hint text to be sure users know what it's for. # LANG: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL - nb.Label(conf_frame, text=_('Override Beta/Normal Selection')).grid( + nb.Label(conf_frame, text=tr.tl('Override Beta/Normal Selection')).grid( sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY ) nb.OptionMenu( conf_frame, coriolis_config.override_textvar, coriolis_config.override_textvar.get(), - _('Normal'), # LANG: 'Normal' label for Coriolis site override selection - _('Beta'), # LANG: 'Beta' label for Coriolis site override selection - _('Auto') # LANG: 'Auto' label for Coriolis site override selection + tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + tr.tl('Beta'), # LANG: 'Beta' label for Coriolis site override selection + tr.tl('Auto') # LANG: 'Auto' label for Coriolis site override selection ).grid(sticky=tk.W, row=cur_row, column=1, padx=PADX, pady=BOXY) cur_row += 1 @@ -152,15 +155,35 @@ def prefs_changed(cmdr: str | None, is_beta: bool) -> None: # Convert to unlocalised names coriolis_config.override_mode = { - _('Normal'): 'normal', # LANG: Coriolis normal/beta selection - normal - _('Beta'): 'beta', # LANG: Coriolis normal/beta selection - beta - _('Auto'): 'auto', # LANG: Coriolis normal/beta selection - auto + tr.tl('Normal'): 'normal', # LANG: Coriolis normal/beta selection - normal + tr.tl('Beta'): 'beta', # LANG: Coriolis normal/beta selection - beta + tr.tl('Auto'): 'auto', # LANG: Coriolis normal/beta selection - auto }.get(coriolis_config.override_mode, coriolis_config.override_mode) + # Check if the language was changed and the override_mode was valid before the change + if coriolis_config.override_mode not in ('beta', 'normal', 'auto'): + coriolis_config.override_mode = { + coriolis_config.override_text_old_normal: 'normal', + coriolis_config.override_text_old_beta: 'beta', + coriolis_config.override_text_old_auto: 'auto', + }.get(coriolis_config.override_mode, coriolis_config.override_mode) + # Language was seemingly changed, so we need to update the textvars + if coriolis_config.override_mode in ('beta', 'normal', 'auto'): + coriolis_config.override_textvar.set( + value={ + 'auto': tr.tl('Auto'), # LANG: 'Auto' label for Coriolis site override selection + 'normal': tr.tl('Normal'), # LANG: 'Normal' label for Coriolis site override selection + 'beta': tr.tl('Beta') # LANG: 'Beta' label for Coriolis site override selection + # LANG: 'Auto' label for Coriolis site override selection + }.get(coriolis_config.override_mode, tr.tl('Auto')) + ) + + # If the override mode is still invalid, default to auto if coriolis_config.override_mode not in ('beta', 'normal', 'auto'): logger.warning(f'Unexpected value {coriolis_config.override_mode=!r}. Defaulting to "auto"') coriolis_config.override_mode = 'auto' - coriolis_config.override_textvar.set(value=_('Auto')) # LANG: 'Auto' label for Coriolis site override selection + # LANG: 'Auto' label for Coriolis site override selection + coriolis_config.override_textvar.set(value=tr.tl('Auto')) config.set('coriolis_normal_url', coriolis_config.normal_url) config.set('coriolis_beta_url', coriolis_config.beta_url) @@ -170,7 +193,7 @@ def prefs_changed(cmdr: str | None, is_beta: bool) -> None: def _get_target_url(is_beta: bool) -> str: if coriolis_config.override_mode not in ('auto', 'normal', 'beta'): # LANG: Settings>Coriolis - invalid override mode found - show_error(_('Invalid Coriolis override mode!')) + show_error(tr.tl('Invalid Coriolis override mode!')) logger.warning(f'Unexpected override mode {coriolis_config.override_mode!r}! defaulting to auto!') coriolis_config.override_mode = 'auto' if coriolis_config.override_mode == 'beta': diff --git a/plugins/eddn.py b/plugins/eddn.py index 687b39b7..01d0a46a 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -27,20 +27,11 @@ import os import pathlib import re import sqlite3 -import sys import tkinter as tk -from collections import OrderedDict from platform import system from textwrap import dedent from threading import Lock -from typing import ( - TYPE_CHECKING, - Any, - Iterator, - Mapping, - MutableMapping, -) -from typing import OrderedDict as OrderedDictT +from typing import Any, Iterator, Mapping, MutableMapping import requests import companion import edmc_data @@ -55,10 +46,7 @@ from myNotebook import Frame from prefs import prefsVersion from ttkHyperlinkLabel import HyperlinkLabel from util import text - -if TYPE_CHECKING: - def _(x: str) -> str: - return x +from l10n import translations as tr logger = get_main_logger() @@ -98,13 +86,13 @@ class This: # Avoid duplicates self.marketId: str | None = None - self.commodities: list[OrderedDictT[str, Any]] | None = None + self.commodities: list[dict[str, Any]] | None = None self.outfitting: tuple[bool, list[str]] | None = None self.shipyard: tuple[bool, list[Mapping[str, Any]]] | None = None self.fcmaterials_marketid: int = 0 - self.fcmaterials: list[OrderedDictT[str, Any]] | None = None + self.fcmaterials: list[dict[str, Any]] | None = None self.fcmaterials_capi_marketid: int = 0 - self.fcmaterials_capi: list[OrderedDictT[str, Any]] | None = None + self.fcmaterials_capi: list[dict[str, Any]] | None = None # For the tkinter parent window, so we can call update_idletasks() self.parent: tk.Tk @@ -284,7 +272,7 @@ class EDDNSender: msg['header'] = { # We have to lie and say it's *this* version, but denote that # it might not actually be this version. - 'softwareName': f'{applongname} [{system() if sys.platform != "darwin" else "Mac OS"}]' + 'softwareName': f'{applongname} [{system()}]' ' (legacy replay)', 'softwareVersion': str(appversion_nobuild()), 'uploaderID': cmdr, @@ -444,7 +432,7 @@ class EDDNSender: except requests.exceptions.RequestException as e: logger.debug('Failed sending', exc_info=e) # LANG: Error while trying to send data to EDDN - self.set_ui_status(_("Error: Can't connect to EDDN")) + self.set_ui_status(tr.tl("Error: Can't connect to EDDN")) except Exception as e: logger.debug('Failed sending', exc_info=e) @@ -569,17 +557,17 @@ class EDDNSender: if status_code == 429: # HTTP UPGRADE REQUIRED logger.warning('EDMC is sending schemas that are too old') # LANG: EDDN has banned this version of our client - return _('EDDN Error: EDMC is too old for EDDN. Please update.') + return tr.tl('EDDN Error: EDMC is too old for EDDN. Please update.') if status_code == 400: # we a validation check or something else. logger.warning(f'EDDN Error: {status_code} -- {exception.response}') # LANG: EDDN returned an error that indicates something about what we sent it was wrong - return _('EDDN Error: Validation Failed (EDMC Too Old?). See Log') + return tr.tl('EDDN Error: Validation Failed (EDMC Too Old?). See Log') logger.warning(f'Unknown status code from EDDN: {status_code} -- {exception.response}') # LANG: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number - return _('EDDN Error: Returned {STATUS} status code').format(STATUS=status_code) + return tr.tl('EDDN Error: Returned {STATUS} status code').format(STATUS=status_code) # TODO: a good few of these methods are static or could be classmethods. they should be created as such. @@ -651,21 +639,21 @@ class EDDN: modules, ships ) - commodities: list[OrderedDictT[str, Any]] = [] + commodities: list[dict[str, Any]] = [] for commodity in data['lastStarport'].get('commodities') or []: # Check 'marketable' and 'not prohibited' if (category_map.get(commodity['categoryname'], True) and not commodity.get('legality')): - commodities.append(OrderedDict([ - ('name', commodity['name'].lower()), - ('meanPrice', int(commodity['meanPrice'])), - ('buyPrice', int(commodity['buyPrice'])), - ('stock', int(commodity['stock'])), - ('stockBracket', commodity['stockBracket']), - ('sellPrice', int(commodity['sellPrice'])), - ('demand', int(commodity['demand'])), - ('demandBracket', commodity['demandBracket']), - ])) + commodities.append({ + 'name': commodity['name'].lower(), + 'meanPrice': int(commodity['meanPrice']), + 'buyPrice': int(commodity['buyPrice']), + 'stock': int(commodity['stock']), + 'stockBracket': commodity['stockBracket'], + 'sellPrice': int(commodity['sellPrice']), + 'demand': int(commodity['demand']), + 'demandBracket': commodity['demandBracket'], + }) if commodity['statusFlags']: commodities[-1]['statusFlags'] = commodity['statusFlags'] @@ -679,15 +667,15 @@ class EDDN: # none and that really does need to be recorded over EDDN so that # tools can update in a timely manner. if this.commodities != commodities: - message: OrderedDictT[str, Any] = OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('commodities', commodities), - ('horizons', horizons), - ('odyssey', this.odyssey), - ]) + message: dict[str, Any] = { + 'timestamp': data['timestamp'], + 'systemName': data['lastSystem']['name'], + 'stationName': data['lastStarport']['name'], + 'marketId': data['lastStarport']['id'], + 'commodities': commodities, + 'horizons': horizons, + 'odyssey': this.odyssey, + } if 'economies' in data['lastStarport']: message['economies'] = sorted( @@ -802,16 +790,16 @@ class EDDN: if outfitting and this.outfitting != (horizons, outfitting): self.send_message(data['commander']['name'], { '$schemaRef': f'https://eddn.edcd.io/schemas/outfitting/2{"/test" if is_beta else ""}', - 'message': OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('horizons', horizons), - ('modules', outfitting), - ('odyssey', this.odyssey), - ]), - 'header': self.standard_header( + 'message': { + 'timestamp': data['timestamp'], + 'systemName': data['lastSystem']['name'], + 'stationName': data['lastStarport']['name'], + 'marketId': data['lastStarport']['id'], + 'horizons': horizons, + 'modules': outfitting, + 'odyssey': this.odyssey, + }, + 'header': self.standard_header( game_version=self.capi_gameversion_from_host_endpoint( data.source_host, companion.Session.FRONTIER_CAPI_PATH_SHIPYARD ), @@ -864,15 +852,15 @@ class EDDN: if shipyard and this.shipyard != (horizons, shipyard): self.send_message(data['commander']['name'], { '$schemaRef': f'https://eddn.edcd.io/schemas/shipyard/2{"/test" if is_beta else ""}', - 'message': OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('horizons', horizons), - ('ships', shipyard), - ('odyssey', this.odyssey), - ]), + 'message': { + 'timestamp': data['timestamp'], + 'systemName': data['lastSystem']['name'], + 'stationName': data['lastStarport']['name'], + 'marketId': data['lastStarport']['id'], + 'horizons': horizons, + 'ships': shipyard, + 'odyssey': this.odyssey, + }, 'header': self.standard_header( game_version=self.capi_gameversion_from_host_endpoint( data.source_host, companion.Session.FRONTIER_CAPI_PATH_SHIPYARD @@ -898,16 +886,22 @@ class EDDN: :param entry: the journal entry containing the commodities data """ items: list[Mapping[str, Any]] = entry.get('Items') or [] - commodities: list[OrderedDictT[str, Any]] = sorted((OrderedDict([ - ('name', self.canonicalise(commodity['Name'])), - ('meanPrice', commodity['MeanPrice']), - ('buyPrice', commodity['BuyPrice']), - ('stock', commodity['Stock']), - ('stockBracket', commodity['StockBracket']), - ('sellPrice', commodity['SellPrice']), - ('demand', commodity['Demand']), - ('demandBracket', commodity['DemandBracket']), - ]) for commodity in items), key=lambda c: c['name']) + commodities: list[dict[str, Any]] = sorted( + ( + { + 'name': self.canonicalise(commodity['Name']), + 'meanPrice': commodity['MeanPrice'], + 'buyPrice': commodity['BuyPrice'], + 'stock': commodity['Stock'], + 'stockBracket': commodity['StockBracket'], + 'sellPrice': commodity['SellPrice'], + 'demand': commodity['Demand'], + 'demandBracket': commodity['DemandBracket'], + } + for commodity in items + ), + key=lambda c: c['name'] + ) # This used to have a check `commodities and ` at the start so as to # not send an empty commodities list, as the EDDN Schema doesn't allow @@ -916,18 +910,24 @@ class EDDN: # none and that really does need to be recorded over EDDN so that # tools can update in a timely manner. if this.commodities != commodities: - self.send_message(cmdr, { + message: dict[str, Any] = { # Yes, this is a broad type hint. '$schemaRef': f'https://eddn.edcd.io/schemas/commodity/3{"/test" if is_beta else ""}', - 'message': OrderedDict([ - ('timestamp', entry['timestamp']), - ('systemName', entry['StarSystem']), - ('stationName', entry['StationName']), - ('marketId', entry['MarketID']), - ('commodities', commodities), - ('horizons', this.horizons), - ('odyssey', this.odyssey), - ]), - }) + 'message': { + 'timestamp': entry['timestamp'], + 'systemName': entry['StarSystem'], + 'stationName': entry['StationName'], + 'marketId': entry['MarketID'], + 'commodities': commodities, + 'horizons': this.horizons, + 'odyssey': this.odyssey, + 'stationType': entry['StationType'], + } + } + + if entry.get('CarrierDockingAccess'): + message['message']['carrierDockingAccess'] = entry['CarrierDockingAccess'] + + self.send_message(cmdr, message) this.commodities = commodities @@ -957,15 +957,15 @@ class EDDN: if outfitting and this.outfitting != (horizons, outfitting): self.send_message(cmdr, { '$schemaRef': f'https://eddn.edcd.io/schemas/outfitting/2{"/test" if is_beta else ""}', - 'message': OrderedDict([ - ('timestamp', entry['timestamp']), - ('systemName', entry['StarSystem']), - ('stationName', entry['StationName']), - ('marketId', entry['MarketID']), - ('horizons', horizons), - ('modules', outfitting), - ('odyssey', entry['odyssey']) - ]), + 'message': { + 'timestamp': entry['timestamp'], + 'systemName': entry['StarSystem'], + 'stationName': entry['StationName'], + 'marketId': entry['MarketID'], + 'horizons': horizons, + 'modules': outfitting, + 'odyssey': entry['odyssey'] + }, }) this.outfitting = (horizons, outfitting) @@ -991,15 +991,15 @@ class EDDN: if shipyard and this.shipyard != (horizons, shipyard): self.send_message(cmdr, { '$schemaRef': f'https://eddn.edcd.io/schemas/shipyard/2{"/test" if is_beta else ""}', - 'message': OrderedDict([ - ('timestamp', entry['timestamp']), - ('systemName', entry['StarSystem']), - ('stationName', entry['StationName']), - ('marketId', entry['MarketID']), - ('horizons', horizons), - ('ships', shipyard), - ('odyssey', entry['odyssey']) - ]), + 'message': { + 'timestamp': entry['timestamp'], + 'systemName': entry['StarSystem'], + 'stationName': entry['StationName'], + 'marketId': entry['MarketID'], + 'horizons': horizons, + 'ships': shipyard, + 'odyssey': entry['odyssey'] + }, }) # this.shipyard = (horizons, shipyard) @@ -1064,7 +1064,7 @@ class EDDN: gb = this.game_build return { - 'softwareName': f'{applongname} [{system() if sys.platform != "darwin" else "Mac OS"}]', + 'softwareName': f'{applongname} [{system()}]', 'softwareVersion': str(appversion_nobuild()), 'uploaderID': this.cmdr_name, 'gameversion': gv, @@ -1416,15 +1416,6 @@ class EDDN: ####################################################################### # Elisions ####################################################################### - # WORKAROUND WIP EDDN schema | 2021-10-17: This will reject with the Odyssey or Horizons flags present - if 'odyssey' in entry: - del entry['odyssey'] - - if 'horizons' in entry: - del entry['horizons'] - - # END WORKAROUND - # In case Frontier ever add any entry = filter_localised(entry) ####################################################################### @@ -1880,6 +1871,74 @@ class EDDN: return None + def export_journal_dockingdenied( + self, cmdr: str, is_beta: bool, entry: Mapping[str, Any] + ) -> str | None: + """ + Send a DockingDenied to EDDN on the correct schema. + + :param cmdr: the commander under which this upload is made + :param is_beta: whether or not we are in beta mode + :param entry: the journal entry to send + ___ + Example: + { + "timestamp":"2022-06-10T10:09:41Z", + "event":"DockingDenied", + "Reason":"RestrictedAccess", + "MarketID":3706117376, + "StationName":"V7G-T1G", + "StationType":"FleetCarrier" + } + """ + ####################################################################### + # Elisions + ####################################################################### + # In case Frontier ever add any + entry = filter_localised(entry) + + msg = { + '$schemaRef': f'https://eddn.edcd.io/schemas/dockingdenied/1{"/test" if is_beta else ""}', + 'message': entry + } + + this.eddn.send_message(cmdr, msg) + return None + + def export_journal_dockinggranted( + self, cmdr: str, is_beta: bool, entry: Mapping[str, Any] + ) -> str | None: + """ + Send a DockingDenied to EDDN on the correct schema. + + :param cmdr: the commander under which this upload is made + :param is_beta: whether or not we are in beta mode + :param entry: the journal entry to send + ___ + Example: + { + "timestamp":"2023-10-01T14:56:34Z", + "event":"DockingGranted", + "LandingPad":41, + "MarketID":3227312896, + "StationName":"Evans Horizons", + "StationType":"Coriolis" + } + """ + ####################################################################### + # Elisions + ####################################################################### + # In case Frontier ever add any + entry = filter_localised(entry) + + msg = { + '$schemaRef': f'https://eddn.edcd.io/schemas/dockinggranted/1{"/test" if is_beta else ""}', + 'message': entry + } + + this.eddn.send_message(cmdr, msg) + return None + def canonicalise(self, item: str) -> str: """ Canonicalise the given commodity name. @@ -2112,7 +2171,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_station_button = nb.Checkbutton( eddnframe, # LANG: Enable EDDN support for station data checkbox label - text=_('Send station data to the Elite Dangerous Data Network'), + text=tr.tl('Send station data to the Elite Dangerous Data Network'), variable=this.eddn_station, command=prefsvarchanged ) # Output setting @@ -2124,7 +2183,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_system_button = nb.Checkbutton( eddnframe, # LANG: Enable EDDN support for system and other scan data checkbox label - text=_('Send system and scan data to the Elite Dangerous Data Network'), + text=tr.tl('Send system and scan data to the Elite Dangerous Data Network'), variable=this.eddn_system, command=prefsvarchanged ) @@ -2136,7 +2195,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_delay_button = nb.Checkbutton( eddnframe, # LANG: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this - text=_('Delay sending until docked'), + text=tr.tl('Delay sending until docked'), variable=this.eddn_delay ) this.eddn_delay_button.grid(row=cur_row, padx=BUTTONX, pady=PADY, sticky=tk.W) @@ -2182,14 +2241,14 @@ def plugin_stop() -> None: logger.debug('Done.') -def filter_localised(d: Mapping[str, Any]) -> OrderedDictT[str, Any]: +def filter_localised(d: Mapping[str, Any]) -> dict[str, Any]: """ Recursively remove any dict keys with names ending `_Localised` from a dict. :param d: dict to filter keys of. :return: The filtered dict. """ - filtered: OrderedDictT[str, Any] = OrderedDict() + filtered: dict[str, Any] = {} for k, v in d.items(): if k.endswith('_Localised'): pass @@ -2206,14 +2265,14 @@ def filter_localised(d: Mapping[str, Any]) -> OrderedDictT[str, Any]: return filtered -def capi_filter_localised(d: Mapping[str, Any]) -> OrderedDictT[str, Any]: +def capi_filter_localised(d: Mapping[str, Any]) -> dict[str, Any]: """ Recursively remove any dict keys for known CAPI 'localised' names. :param d: dict to filter keys of. :return: The filtered dict. """ - filtered: OrderedDictT[str, Any] = OrderedDict() + filtered: dict[str, Any] = {} for k, v in d.items(): if EDDN.CAPI_LOCALISATION_RE.search(k): pass @@ -2251,7 +2310,7 @@ def journal_entry( # noqa: C901, CCR001 """ should_return, new_data = killswitch.check_killswitch('plugins.eddn.journal', entry) if should_return: - plug.show_error(_('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN + plug.show_error(tr.tl('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN return None should_return, new_data = killswitch.check_killswitch(f'plugins.eddn.journal.event.{entry["event"]}', new_data) @@ -2327,6 +2386,12 @@ def journal_entry( # noqa: C901, CCR001 if event_name == 'fcmaterials': return this.eddn.export_journal_fcmaterials(cmdr, is_beta, entry) + if event_name == "dockingdenied": + return this.eddn.export_journal_dockingdenied(cmdr, is_beta, entry) + + if event_name == "dockinggranted": + return this.eddn.export_journal_dockinggranted(cmdr, is_beta, entry) + if event_name == 'approachsettlement': # An `ApproachSettlement` can appear *before* `Location` if you # logged at one. We won't have necessary augmentation data @@ -2447,7 +2512,7 @@ def journal_entry( # noqa: C901, CCR001 except requests.exceptions.RequestException as e: logger.debug('Failed in send_message', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug('Failed in export_journal_generic', exc_info=e) @@ -2485,7 +2550,7 @@ def journal_entry( # noqa: C901, CCR001 except requests.exceptions.RequestException as e: logger.debug(f'Failed exporting {entry["event"]}', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug(f'Failed exporting {entry["event"]}', exc_info=e) @@ -2539,7 +2604,8 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 status = this.parent.nametowidget(f".{appname.lower()}.status") old_status = status['text'] if not old_status: - status['text'] = _('Sending data to EDDN...') # LANG: Status text shown while attempting to send data + # LANG: Status text shown while attempting to send data + status['text'] = tr.tl('Sending data to EDDN...') status.update_idletasks() this.eddn.export_commodities(data, is_beta) @@ -2551,7 +2617,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 except requests.RequestException as e: logger.debug('Failed exporting data', exc_info=e) - return _("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN + return tr.tl("Error: Can't connect to EDDN") # LANG: Error while trying to send data to EDDN except Exception as e: logger.debug('Failed exporting data', exc_info=e) diff --git a/plugins/edsm.py b/plugins/edsm.py index 754ffb61..1d9f8cb8 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -28,7 +28,7 @@ from queue import Queue from threading import Thread from time import sleep from tkinter import ttk -from typing import TYPE_CHECKING, Any, Literal, Mapping, MutableMapping, cast +from typing import Any, Literal, Mapping, MutableMapping, cast, Sequence import requests import killswitch import monitor @@ -39,10 +39,8 @@ from config import applongname, appname, appversion, config, debug_senders, user from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel +from l10n import translations as tr -if TYPE_CHECKING: - def _(x: str) -> str: - return x # TODO: # 1) Re-factor EDSM API calls out of journal_entry() into own function. @@ -113,10 +111,10 @@ class This: self.cmdr_text: nb.Label | None = None self.user_label: nb.Label | None = None - self.user: nb.Entry | None = None + self.user: nb.EntryMenu | None = None self.apikey_label: nb.Label | None = None - self.apikey: nb.Entry | None = None + self.apikey: nb.EntryMenu | None = None this = This() @@ -279,7 +277,7 @@ def toggle_password_visibility(): this.apikey.config(show="*") # type: ignore -def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: """ Plugin preferences setup hook. @@ -313,7 +311,8 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr this.log = tk.IntVar(value=config.get_int('edsm_out') and 1) this.log_button = nb.Checkbutton( frame, - text=_('Send flight log and CMDR status to EDSM'), # LANG: Settings>EDSM - Label on checkbox for 'send data' + # LANG: Settings>EDSM - Label on checkbox for 'send data' + text=tr.tl('Send flight log and CMDR status to EDSM'), variable=this.log, command=prefsvarchanged ) @@ -328,7 +327,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr this.label = HyperlinkLabel( frame, - text=_('Elite Dangerous Star Map credentials'), # LANG: Elite Dangerous Star Map credentials + text=tr.tl('Elite Dangerous Star Map credentials'), # LANG: Elite Dangerous Star Map credentials background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True @@ -336,23 +335,23 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr if this.label: this.label.grid(row=cur_row, columnspan=2, padx=PADX, pady=PADY, sticky=tk.W) cur_row += 1 - this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # LANG: Game Commander name label in EDSM settings + this.cmdr_label = nb.Label(frame, text=tr.tl('Cmdr')) # LANG: Game Commander name label in EDSM settings this.cmdr_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) this.cmdr_text = nb.Label(frame) this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.W) cur_row += 1 # LANG: EDSM Commander name label in EDSM settings - this.user_label = nb.Label(frame, text=_('Commander Name')) + this.user_label = nb.Label(frame, text=tr.tl('Commander Name')) this.user_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.user = nb.Entry(frame) + this.user = nb.EntryMenu(frame) this.user.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 # LANG: EDSM API key label - this.apikey_label = nb.Label(frame, text=_('API Key')) + this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.Entry(frame, show="*", width=50) + this.apikey = nb.EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 @@ -362,7 +361,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr show_password_checkbox = nb.Checkbutton( frame, - text=_('Show API Key'), # LANG: Text EDSM Show API Key + text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key variable=show_password_var, command=toggle_password_visibility ) @@ -398,7 +397,7 @@ def prefs_cmdr_changed(cmdr: str | None, is_beta: bool) -> None: # noqa: CCR001 else: if this.cmdr_text: # LANG: We have no data on the current commander - this.cmdr_text['text'] = _('None') + this.cmdr_text['text'] = tr.tl('None') to_set: Literal['normal'] | Literal['disabled'] = tk.DISABLED if cmdr and not is_beta and this.log and this.log.get(): @@ -492,6 +491,20 @@ def credentials(cmdr: str) -> tuple[str, str] | None: edsm_usernames = config.get_list('edsm_usernames') edsm_apikeys = config.get_list('edsm_apikeys') + if not edsm_usernames: # https://github.com/EDCD/EDMarketConnector/issues/2232 + edsm_usernames = ["" for _ in range(len(cmdrs))] + else: # Check for Mismatched Length - fill with null values. + if len(edsm_usernames) < len(cmdrs): + edsm_usernames.extend(["" for _ in range(len(cmdrs) - len(edsm_usernames))]) + config.set('edsm_usernames', edsm_usernames) + + if not edsm_apikeys: + edsm_apikeys = ["" for _ in range(len(cmdrs))] + else: # Check for Mismatched Length - fill with null values. + if len(edsm_apikeys) < len(cmdrs): + edsm_apikeys.extend(["" for _ in range(len(cmdrs) - len(edsm_apikeys))]) + config.set('edsm_apikeys', edsm_apikeys) + if cmdr in cmdrs and len(cmdrs) == len(edsm_usernames) == len(edsm_apikeys): idx = cmdrs.index(cmdr) if idx < len(edsm_usernames) and idx < len(edsm_apikeys): @@ -519,7 +532,7 @@ def journal_entry( # noqa: C901, CCR001 should_return, new_entry = killswitch.check_killswitch('plugins.edsm.journal', entry, logger) if should_return: # LANG: EDSM plugin - Journal handling disabled by killswitch - plug.show_error(_('EDSM Handler disabled. See Log.')) + plug.show_error(tr.tl('EDSM Handler disabled. See Log.')) return '' should_return, new_entry = killswitch.check_killswitch( @@ -606,7 +619,7 @@ entry: {entry!r}''' # LANG: The Inara API only accepts Live galaxy data, not Legacy galaxy data logger.info("EDSM only accepts Live galaxy data") this.legacy_galaxy_last_notified = datetime.now(timezone.utc) - return _("EDSM only accepts Live galaxy data") # LANG: EDSM - Only Live data + return tr.tl("EDSM only accepts Live galaxy data") # LANG: EDSM - Only Live data return '' @@ -722,6 +735,87 @@ def get_discarded_events_list() -> None: logger.warning('Exception while trying to set this.discarded_events:', exc_info=e) +def process_discarded_events() -> None: + """Process discarded events until the discarded events list is retrieved or the shutdown signal is received.""" + while not this.discarded_events: + if this.shutting_down: + logger.debug(f'returning from discarded_events loop due to {this.shutting_down=}') + return + get_discarded_events_list() + if this.discarded_events: + break + sleep(DISCARDED_EVENTS_SLEEP) + + logger.debug('Got "events to discard" list, commencing queue consumption...') + + +def send_to_edsm( # noqa: CCR001 + data: dict[str, Sequence[object]], pending: list[Mapping[str, Any]], closing: bool +) -> list[Mapping[str, Any]]: + """Send data to the EDSM API endpoint and handle the API response.""" + response = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT) + logger.trace_if('plugin.edsm.api', f'API response content: {response.content!r}') + + # Check for rate limit headers + rate_limit_remaining = response.headers.get('X-Rate-Limit-Remaining') + rate_limit_reset = response.headers.get('X-Rate-Limit-Reset') + + # Convert headers to integers if they exist + try: + remaining = int(rate_limit_remaining) if rate_limit_remaining else None + reset = int(rate_limit_reset) if rate_limit_reset else None + except ValueError: + remaining = reset = None + + if remaining is not None and reset is not None: + # Respect rate limits if they exist + if remaining == 0: + # Calculate sleep time until the rate limit reset time + reset_time = datetime.utcfromtimestamp(reset) + current_time = datetime.utcnow() + + sleep_time = (reset_time - current_time).total_seconds() + + if sleep_time > 0: + sleep(sleep_time) + + response.raise_for_status() + reply = response.json() + msg_num = reply['msgnum'] + msg = reply['msg'] + # 1xx = OK + # 2xx = fatal error + # 3&4xx not generated at top-level + # 5xx = error but events saved for later processing + + if msg_num // 100 == 2: + logger.warning(f'EDSM\t{msg_num} {msg}\t{json.dumps(pending, separators=(",", ": "))}') + # LANG: EDSM Plugin - Error message from EDSM API + plug.show_error(tr.tl('Error: EDSM {MSG}').format(MSG=msg)) + else: + if msg_num // 100 == 1: + logger.trace_if('plugin.edsm.api', 'Overall OK') + pass + elif msg_num // 100 == 5: + logger.trace_if('plugin.edsm.api', 'Event(s) not currently processed, but saved for later') + pass + else: + logger.warning(f'EDSM API call status not 1XX, 2XX or 5XX: {msg.num}') + + for e, r in zip(pending, reply['events']): + if not closing and e['event'] in ('StartUp', 'Location', 'FSDJump', 'CarrierJump'): + # Update main window's system status + this.lastlookup = r + # calls update_status in main thread + if not config.shutting_down and this.system_link is not None: + this.system_link.event_generate('<>', when="tail") + if r['msgnum'] // 100 != 1: + logger.warning(f'EDSM event with not-1xx status:\n{r["msgnum"]}\n' + f'{r["msg"]}\n{json.dumps(e, separators=(",", ": "))}') + pending = [] + return pending + + def worker() -> None: # noqa: CCR001 C901 """ Handle uploading events to EDSM API. @@ -738,17 +832,9 @@ def worker() -> None: # noqa: CCR001 C901 last_game_version = "" last_game_build = "" - while not this.discarded_events: - if this.shutting_down: - logger.debug(f'returning from discarded_events loop due to {this.shutting_down=}') - return - get_discarded_events_list() - if this.discarded_events: - break + # Process the Discard Queue + process_discarded_events() - sleep(DISCARDED_EVENTS_SLEEP) - - logger.debug('Got "events to discard" list, commencing queue consumption...') while True: if this.shutting_down: logger.debug(f'{this.shutting_down=}, so setting closing = True') @@ -861,43 +947,8 @@ def worker() -> None: # noqa: CCR001 C901 'journal.locations', f'Overall POST data (elided) is:\n{json.dumps(data_elided, indent=2)}' ) - response = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT) - logger.trace_if('plugin.edsm.api', f'API response content: {response.content!r}') - response.raise_for_status() + pending = send_to_edsm(data, pending, closing) - reply = response.json() - msg_num = reply['msgnum'] - msg = reply['msg'] - # 1xx = OK - # 2xx = fatal error - # 3&4xx not generated at top-level - # 5xx = error but events saved for later processing - - if msg_num // 100 == 2: - logger.warning(f'EDSM\t{msg_num} {msg}\t{json.dumps(pending, separators=(",", ": "))}') - # LANG: EDSM Plugin - Error message from EDSM API - plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) - else: - if msg_num // 100 == 1: - logger.trace_if('plugin.edsm.api', 'Overall OK') - pass - elif msg_num // 100 == 5: - logger.trace_if('plugin.edsm.api', 'Event(s) not currently processed, but saved for later') - pass - else: - logger.warning(f'EDSM API call status not 1XX, 2XX or 5XX: {msg.num}') - - for e, r in zip(pending, reply['events']): - if not closing and e['event'] in ('StartUp', 'Location', 'FSDJump', 'CarrierJump'): - # Update main window's system status - this.lastlookup = r - # calls update_status in main thread - if not config.shutting_down and this.system_link is not None: - this.system_link.event_generate('<>', when="tail") - if r['msgnum'] // 100 != 1: - logger.warning(f'EDSM event with not-1xx status:\n{r["msgnum"]}\n' - f'{r["msg"]}\n{json.dumps(e, separators = (",", ": "))}') - pending = [] break # No exception, so assume success except Exception as e: @@ -906,7 +957,7 @@ def worker() -> None: # noqa: CCR001 C901 else: # LANG: EDSM Plugin - Error connecting to EDSM API - plug.show_error(_("Error: Can't connect to EDSM")) + plug.show_error(tr.tl("Error: Can't connect to EDSM")) if entry['event'].lower() in ('shutdown', 'commander', 'fileheader'): # Game shutdown or new login, so we MUST not hang on to pending pending = [] @@ -980,11 +1031,11 @@ def edsm_notify_system(reply: Mapping[str, Any]) -> None: if not reply: this.system_link['image'] = this._IMG_ERROR # LANG: EDSM Plugin - Error connecting to EDSM API - plug.show_error(_("Error: Can't connect to EDSM")) + plug.show_error(tr.tl("Error: Can't connect to EDSM")) elif reply['msgnum'] // 100 not in (1, 4): this.system_link['image'] = this._IMG_ERROR # LANG: EDSM Plugin - Error message from EDSM API - plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) + plug.show_error(tr.tl('Error: EDSM {MSG}').format(MSG=reply['msg'])) elif reply.get('systemCreated'): this.system_link['image'] = this._IMG_NEW else: diff --git a/plugins/inara.py b/plugins/inara.py index 199e6a1c..b48609f0 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -24,14 +24,13 @@ import json import threading import time import tkinter as tk -from collections import OrderedDict, defaultdict, deque +from collections import defaultdict, deque from dataclasses import dataclass from datetime import datetime, timedelta, timezone from operator import itemgetter from threading import Lock, Thread from tkinter import ttk -from typing import TYPE_CHECKING, Any, Callable, Deque, Mapping, NamedTuple, Sequence, cast, Union -from typing import OrderedDict as OrderedDictT +from typing import Any, Callable, Deque, Mapping, NamedTuple, Sequence, cast, Union import requests import edmc_data import killswitch @@ -43,13 +42,10 @@ from config import applongname, appname, appversion, config, debug_senders from EDMCLogging import get_main_logger from monitor import monitor from ttkHyperlinkLabel import HyperlinkLabel +from l10n import translations as tr logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: - return x - _TIMEOUT = 20 FAKE = ('CQC', 'Training', 'Destination') # Fake systems that shouldn't be sent to Inara @@ -102,12 +98,12 @@ class This: self.newsession: bool = True # starting a new session - wait for Cargo event self.undocked: bool = False # just undocked self.suppress_docked = False # Skip initial Docked event if started docked - self.cargo: list[OrderedDictT[str, Any]] | None = None - self.materials: list[OrderedDictT[str, Any]] | None = None + self.cargo: list[dict[str, Any]] | None = None + self.materials: list[dict[str, Any]] | None = None self.last_credits: int = 0 # Send credit update soon after Startup / new game - self.storedmodules: list[OrderedDictT[str, Any]] | None = None - self.loadout: OrderedDictT[str, Any] | None = None - self.fleet: list[OrderedDictT[str, Any]] | None = None + self.storedmodules: list[dict[str, Any]] | None = None + self.loadout: dict[str, Any] | None = None + self.fleet: list[dict[str, Any]] | None = None self.shipswap: bool = False # just swapped ship self.on_foot = False @@ -126,7 +122,7 @@ class This: self.log: 'tk.IntVar' self.log_button: nb.Checkbutton self.label: HyperlinkLabel - self.apikey: nb.Entry + self.apikey: nb.EntryMenu self.apikey_label: tk.Label self.events: dict[Credentials, Deque[Event]] = defaultdict(deque) @@ -245,7 +241,7 @@ def toggle_password_visibility(): this.apikey.config(show="*") -def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: """Plugin Preferences UI hook.""" PADX = 10 # noqa: N806 BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons @@ -265,7 +261,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: this.log = tk.IntVar(value=config.get_int('inara_out') and 1) this.log_button = nb.Checkbutton( frame, - text=_('Send flight log and Cmdr status to Inara'), # LANG: Checkbox to enable INARA API Usage + text=tr.tl('Send flight log and Cmdr status to Inara'), # LANG: Checkbox to enable INARA API Usage variable=this.log, command=prefsvarchanged ) @@ -281,7 +277,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: # Section heading in settings this.label = HyperlinkLabel( frame, - text=_('Inara credentials'), # LANG: Text for INARA API keys link ( goes to https://inara.cz/settings-api ) + text=tr.tl('Inara credentials'), # LANG: Text for INARA API keys link ( goes to https://inara.cz/settings-api ) background=nb.Label().cget('background'), url='https://inara.cz/settings-api', underline=True @@ -291,9 +287,9 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: cur_row += 1 # LANG: Inara API key label - this.apikey_label = nb.Label(frame, text=_('API Key')) # Inara setting + this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) # Inara setting this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.Entry(frame, show="*", width=50) + this.apikey = nb.EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 @@ -302,7 +298,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: show_password_var.set(False) # Password is initially masked show_password_checkbox = nb.Checkbutton( frame, - text=_('Show API Key'), # LANG: Text Inara Show API key + text=tr.tl('Show API Key'), # LANG: Text Inara Show API key variable=show_password_var, command=toggle_password_visibility, ) @@ -408,7 +404,7 @@ def journal_entry( # noqa: C901, CCR001 should_return, new_entry = killswitch.check_killswitch('plugins.inara.journal', entry, logger) if should_return: - plug.show_error(_('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch + plug.show_error(tr.tl('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch logger.trace('returning due to killswitch match') return '' @@ -433,9 +429,9 @@ def journal_entry( # noqa: C901, CCR001 and config.get_int('inara_out') and not (is_beta or this.multicrew or credentials(cmdr)) ): # LANG: The Inara API only accepts Live galaxy data, not Legacy galaxy data - logger.info(_("Inara only accepts Live galaxy data")) + logger.info(tr.tl("Inara only accepts Live galaxy data")) this.legacy_galaxy_last_notified = datetime.now(timezone.utc) - return _("Inara only accepts Live galaxy data") # LANG: Inara - Only Live data + return tr.tl("Inara only accepts Live galaxy data") # LANG: Inara - Only Live data return '' @@ -552,25 +548,12 @@ def journal_entry( # noqa: C901, CCR001 power_defect_data = {'powerName': entry["ToPower"], 'rankValue': 1} new_add_event('setCommanderRankPower', entry['timestamp'], power_defect_data) + elif event_name == 'Powerplay': + power_data = {'powerName': entry["Power"], 'rankValue': entry["Rank"], 'meritsValue': entry["Merits"]} + new_add_event('setCommanderRankPower', entry['timestamp'], power_data) + # Ship change if event_name == 'Loadout' and this.shipswap: - cur_ship = { - 'shipType': state['ShipType'], - 'shipGameID': state['ShipID'], - 'shipName': state['ShipName'], # Can be None - 'shipIdent': state['ShipIdent'], # Can be None - 'isCurrentShip': True, - } - - if state['HullValue']: - cur_ship['shipHullValue'] = state['HullValue'] - - if state['ModulesValue']: - cur_ship['shipModulesValue'] = state['ModulesValue'] - - cur_ship['shipRebuyCost'] = state['Rebuy'] - new_add_event('setCommanderShip', entry['timestamp'], cur_ship) - this.loadout = make_loadout(state) new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) this.shipswap = False @@ -721,13 +704,13 @@ def journal_entry( # noqa: C901, CCR001 this.suppress_docked = True # Send cargo and materials if changed - cargo = [OrderedDict({'itemName': k, 'itemCount': state['Cargo'][k]}) for k in sorted(state['Cargo'])] + cargo = [{'itemName': k, 'itemCount': state['Cargo'][k]} for k in sorted(state['Cargo'])] if this.cargo != cargo: new_add_event('setCommanderInventoryCargo', entry['timestamp'], cargo) this.cargo = cargo materials = [ - OrderedDict([('itemName', k), ('itemCount', state[category][k])]) + {'itemName': k, 'itemCount': state[category][k]} for category in ('Raw', 'Manufactured', 'Encoded') for k in sorted(state[category]) ] @@ -823,24 +806,30 @@ def journal_entry( # noqa: C901, CCR001 # Fleet if event_name == 'StoredShips': - fleet: list[OrderedDictT[str, Any]] = sorted( - [OrderedDict({ - 'shipType': x['ShipType'], - 'shipGameID': x['ShipID'], - 'shipName': x.get('Name'), - 'isHot': x['Hot'], - 'starsystemName': entry['StarSystem'], - 'stationName': entry['StationName'], - 'marketID': entry['MarketID'], - }) for x in entry['ShipsHere']] + - [OrderedDict({ - 'shipType': x['ShipType'], - 'shipGameID': x['ShipID'], - 'shipName': x.get('Name'), - 'isHot': x['Hot'], - 'starsystemName': x.get('StarSystem'), # Not present for ships in transit - 'marketID': x.get('ShipMarketID'), # " - }) for x in entry['ShipsRemote']], + fleet = sorted( + [ + { + 'shipType': x['ShipType'], + 'shipGameID': x['ShipID'], + 'shipName': x.get('Name'), + 'isHot': x['Hot'], + 'starsystemName': entry['StarSystem'], + 'stationName': entry['StationName'], + 'marketID': entry['MarketID'], + } + for x in entry['ShipsHere'] + ] + + [ + { + 'shipType': x['ShipType'], + 'shipGameID': x['ShipID'], + 'shipName': x.get('Name'), + 'isHot': x['Hot'], + 'starsystemName': x.get('StarSystem'), # Not present for ships in transit + 'marketID': x.get('ShipMarketID'), # " + } + for x in entry['ShipsRemote'] + ], key=itemgetter('shipGameID') ) @@ -851,9 +840,8 @@ def journal_entry( # noqa: C901, CCR001 # this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent for ship in this.fleet: new_add_event('setCommanderShip', entry['timestamp'], ship) - # Loadout - if event_name == 'Loadout' and not this.newsession: + if event_name == 'Loadout': loadout = make_loadout(state) if this.loadout != loadout: this.loadout = loadout @@ -867,17 +855,37 @@ def journal_entry( # noqa: C901, CCR001 new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) + cur_ship = { + 'shipType': state['ShipType'], + 'shipGameID': state['ShipID'], + 'shipName': state['ShipName'], # Can be None + 'shipIdent': state['ShipIdent'], # Can be None + 'isCurrentShip': True, + 'shipMaxJumpRange': entry['MaxJumpRange'], + 'shipCargoCapacity': entry['CargoCapacity'] + } + if state['HullValue']: + cur_ship['shipHullValue'] = state['HullValue'] + + if state['ModulesValue']: + cur_ship['shipModulesValue'] = state['ModulesValue'] + + if state['Rebuy']: + cur_ship['shipRebuyCost'] = state['Rebuy'] + + new_add_event('setCommanderShip', entry['timestamp'], cur_ship) + # Stored modules if event_name == 'StoredModules': items = {mod['StorageSlot']: mod for mod in entry['Items']} # Impose an order - modules: list[OrderedDictT[str, Any]] = [] + modules: list[dict[str, Any]] = [] for slot in sorted(items): item = items[slot] - module: OrderedDictT[str, Any] = OrderedDict([ - ('itemName', item['Name']), - ('itemValue', item['BuyPrice']), - ('isHot', item['Hot']), - ]) + module: dict[str, Any] = { + 'itemName': item['Name'], + 'itemValue': item['BuyPrice'], + 'isHot': item['Hot'], + } # Location can be absent if in transit if 'StarSystem' in item: @@ -887,7 +895,7 @@ def journal_entry( # noqa: C901, CCR001 module['marketID'] = item['MarketID'] if 'EngineerModifications' in item: - module['engineering'] = OrderedDict([('blueprintName', item['EngineerModifications'])]) + module['engineering'] = {'blueprintName': item['EngineerModifications']} if 'Level' in item: module['engineering']['blueprintLevel'] = item['Level'] @@ -907,18 +915,18 @@ def journal_entry( # noqa: C901, CCR001 # Missions if event_name == 'MissionAccepted': - data: OrderedDictT[str, Any] = OrderedDict([ - ('missionName', entry['Name']), - ('missionGameID', entry['MissionID']), - ('influenceGain', entry['Influence']), - ('reputationGain', entry['Reputation']), - ('starsystemNameOrigin', system), - ('stationNameOrigin', station), - ('minorfactionNameOrigin', entry['Faction']), - ]) + data: dict[str, Any] = { + 'missionName': entry['Name'], + 'missionGameID': entry['MissionID'], + 'influenceGain': entry['Influence'], + 'reputationGain': entry['Reputation'], + 'starsystemNameOrigin': system, + 'stationNameOrigin': station, + 'minorfactionNameOrigin': entry['Faction'], + } # optional mission-specific properties - for (iprop, prop) in [ + for (iprop, prop) in ( ('missionExpiry', 'Expiry'), # Listed as optional in the docs, but always seems to be present ('starsystemNameTarget', 'DestinationSystem'), ('stationNameTarget', 'DestinationStation'), @@ -932,7 +940,7 @@ def journal_entry( # noqa: C901, CCR001 ('passengerCount', 'PassengerCount'), ('passengerIsVIP', 'PassengerVIPs'), ('passengerIsWanted', 'PassengerWanted'), - ]: + ): if prop in entry: data[iprop] = entry[prop] @@ -946,7 +954,7 @@ def journal_entry( # noqa: C901, CCR001 for x in entry.get('PermitsAwarded', []): new_add_event('addCommanderPermit', entry['timestamp'], {'starsystemName': x}) - data = OrderedDict([('missionGameID', entry['MissionID'])]) + data = {'missionGameID': entry['MissionID']} if 'Donation' in entry: data['donationCredits'] = entry['Donation'] @@ -966,7 +974,7 @@ def journal_entry( # noqa: C901, CCR001 factioneffects = [] for faction in entry.get('FactionEffects', []): - effect: OrderedDictT[str, Any] = OrderedDict([('minorfactionName', faction['Faction'])]) + effect: dict[str, Any] = {'minorfactionName': faction['Faction']} for influence in faction.get('Influence', []): if 'Influence' in influence: highest_gain = influence['Influence'] @@ -990,20 +998,32 @@ def journal_entry( # noqa: C901, CCR001 # Combat if event_name == 'Died': - data = OrderedDict([('starsystemName', system)]) + data = {'starsystemName': system} if 'Killers' in entry: data['wingOpponentNames'] = [x['Name'] for x in entry['Killers']] elif 'KillerName' in entry: data['opponentName'] = entry['KillerName'] - new_add_event('addCommanderCombatDeath', entry['timestamp'], data) + elif 'KillerShip' in entry: + data['opponentName'] = entry['KillerShip'] + + # Paranoia in case of e.g. Thargoid activity not having complete data + opponent_name_issue = 'opponentName' not in data or data['opponentName'] == "" + wing_opponent_names_issue = 'wingOpponentNames' not in data or data['wingOpponentNames'] == [] + if opponent_name_issue and wing_opponent_names_issue: + logger.warning('Dropping addCommanderCombatDeath message' + 'because opponentName and wingOpponentNames came out as ""') + + else: + new_add_event('addCommanderCombatDeath', entry['timestamp'], data) elif event_name == 'Interdicted': - data = OrderedDict([('starsystemName', system), - ('isPlayer', entry['IsPlayer']), - ('isSubmit', entry['Submitted']), - ]) + data = { + 'starsystemName': system, + 'isPlayer': entry['IsPlayer'], + 'isSubmit': entry['Submitted'] + } if 'Interdictor' in entry: data['opponentName'] = entry['Interdictor'] @@ -1014,19 +1034,22 @@ def journal_entry( # noqa: C901, CCR001 elif 'Power' in entry: data['opponentName'] = entry['Power'] + elif 'IsThargoid' in entry and entry['IsThargoid']: + data['opponentName'] = 'Thargoid' + # Paranoia in case of e.g. Thargoid activity not having complete data - if data['opponentName'] == "": + if 'opponentName' not in data or data['opponentName'] == "": logger.warning('Dropping addCommanderCombatInterdicted message because opponentName came out as ""') else: new_add_event('addCommanderCombatInterdicted', entry['timestamp'], data) elif event_name == 'Interdiction': - data = OrderedDict([ - ('starsystemName', system), - ('isPlayer', entry['IsPlayer']), - ('isSuccess', entry['Success']), - ]) + data = { + 'starsystemName': system, + 'isPlayer': entry['IsPlayer'], + 'isSuccess': entry['Success'], + } if 'Interdicted' in entry: data['opponentName'] = entry['Interdicted'] @@ -1037,31 +1060,42 @@ def journal_entry( # noqa: C901, CCR001 elif 'Power' in entry: data['opponentName'] = entry['Power'] + # Shouldn't be needed here as Interdiction events can't target Thargoids (yet) + # but done just in case of future changes or so + elif 'IsThargoid' in entry and entry['IsThargoid']: + data['opponentName'] = 'Thargoid' + # Paranoia in case of e.g. Thargoid activity not having complete data - if data['opponentName'] == "": + if 'opponentName' not in data or data['opponentName'] == "": logger.warning('Dropping addCommanderCombatInterdiction message because opponentName came out as ""') else: new_add_event('addCommanderCombatInterdiction', entry['timestamp'], data) elif event_name == 'EscapeInterdiction': + data = { + 'starsystemName': system, + 'isPlayer': entry['IsPlayer'], + } + + if 'Interdictor' in entry: + data['opponentName'] = entry['Interdictor'] + + elif 'Faction' in entry: + data['opponentName'] = entry['Faction'] + + elif 'Power' in entry: + data['opponentName'] = entry['Power'] + + elif 'isThargoid' in entry and entry['isThargoid']: + data['opponentName'] = 'Thargoid' + # Paranoia in case of e.g. Thargoid activity not having complete data - if entry.get('Interdictor') is None or entry['Interdictor'] == "": - logger.warning( - 'Dropping addCommanderCombatInterdictionEscape message' - 'because opponentName came out as ""' - ) + if 'opponentName' not in data or data['opponentName'] == "": + logger.warning('Dropping addCommanderCombatInterdiction message because opponentName came out as ""') else: - new_add_event( - 'addCommanderCombatInterdictionEscape', - entry['timestamp'], - { - 'starsystemName': system, - 'opponentName': entry['Interdictor'], - 'isPlayer': entry['IsPlayer'], - } - ) + new_add_event('addCommanderCombatInterdictionEscape', entry['timestamp'], data) elif event_name == 'PVPKill': new_add_event( @@ -1256,16 +1290,16 @@ def journal_entry( # noqa: C901, CCR001 # )) for goal in entry['CurrentGoals']: - data = OrderedDict([ - ('communitygoalGameID', goal['CGID']), - ('communitygoalName', goal['Title']), - ('starsystemName', goal['SystemName']), - ('stationName', goal['MarketName']), - ('goalExpiry', goal['Expiry']), - ('isCompleted', goal['IsComplete']), - ('contributorsNum', goal['NumContributors']), - ('contributionsTotal', goal['CurrentTotal']), - ]) + data = { + 'communitygoalGameID': goal['CGID'], + 'communitygoalName': goal['Title'], + 'starsystemName': goal['SystemName'], + 'stationName': goal['MarketName'], + 'goalExpiry': goal['Expiry'], + 'isCompleted': goal['IsComplete'], + 'contributorsNum': goal['NumContributors'], + 'contributionsTotal': goal['CurrentTotal'], + } if 'TierReached' in goal: data['tierReached'] = int(goal['TierReached'].split()[-1]) @@ -1279,11 +1313,11 @@ def journal_entry( # noqa: C901, CCR001 new_add_event('setCommunityGoal', entry['timestamp'], data) - data = OrderedDict([ - ('communitygoalGameID', goal['CGID']), - ('contribution', goal['PlayerContribution']), - ('percentileBand', goal['PlayerPercentileBand']), - ]) + data = { + 'communitygoalGameID': goal['CGID'], + 'contribution': goal['PlayerContribution'], + 'percentileBand': goal['PlayerPercentileBand'], + } if 'Bonus' in goal: data['percentileBandReward'] = goal['Bonus'] @@ -1295,7 +1329,7 @@ def journal_entry( # noqa: C901, CCR001 # Friends if event_name == 'Friends': - if entry['Status'] in ['Added', 'Online']: + if entry['Status'] in ('Added', 'Online'): new_add_event( 'addCommanderFriend', entry['timestamp'], @@ -1305,7 +1339,7 @@ def journal_entry( # noqa: C901, CCR001 } ) - elif entry['Status'] in ['Declined', 'Lost']: + elif entry['Status'] in ('Declined', 'Lost'): new_add_event( 'delCommanderFriend', entry['timestamp'], @@ -1380,7 +1414,7 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later pass -def make_loadout(state: dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR001 +def make_loadout(state: dict[str, Any]) -> dict[str, Any]: # noqa: CCR001 """ Construct an inara loadout from an event. @@ -1389,13 +1423,13 @@ def make_loadout(state: dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR0 """ modules = [] for m in state['Modules'].values(): - module: OrderedDictT[str, Any] = OrderedDict([ - ('slotName', m['Slot']), - ('itemName', m['Item']), - ('itemHealth', m['Health']), - ('isOn', m['On']), - ('itemPriority', m['Priority']), - ]) + module: dict[str, Any] = { + 'slotName': m['Slot'], + 'itemName': m['Item'], + 'itemHealth': m['Health'], + 'isOn': m['On'], + 'itemPriority': m['Priority'], + } if 'AmmoInClip' in m: module['itemAmmoClip'] = m['AmmoInClip'] @@ -1410,20 +1444,20 @@ def make_loadout(state: dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR0 module['isHot'] = m['Hot'] if 'Engineering' in m: - engineering: OrderedDictT[str, Any] = OrderedDict([ - ('blueprintName', m['Engineering']['BlueprintName']), - ('blueprintLevel', m['Engineering']['Level']), - ('blueprintQuality', m['Engineering']['Quality']), - ]) + engineering: dict[str, Any] = { + 'blueprintName': m['Engineering']['BlueprintName'], + 'blueprintLevel': m['Engineering']['Level'], + 'blueprintQuality': m['Engineering']['Quality'], + } if 'ExperimentalEffect' in m['Engineering']: engineering['experimentalEffect'] = m['Engineering']['ExperimentalEffect'] engineering['modifiers'] = [] for mod in m['Engineering']['Modifiers']: - modifier: OrderedDictT[str, Any] = OrderedDict([ - ('name', mod['Label']), - ]) + modifier: dict[str, Any] = { + 'name': mod['Label'], + } if 'OriginalValue' in mod: modifier['value'] = mod['Value'] @@ -1439,11 +1473,11 @@ def make_loadout(state: dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR0 modules.append(module) - return OrderedDict([ - ('shipType', state['ShipType']), - ('shipGameID', state['ShipID']), - ('shipLoadout', modules), - ]) + return { + 'shipType': state['ShipType'], + 'shipGameID': state['ShipID'], + 'shipLoadout': modules, + } def new_add_event( @@ -1612,7 +1646,7 @@ def handle_api_error(data: Mapping[str, Any], status: int, reply: dict[str, Any] logger.warning(f'Inara\t{status} {error_message}') logger.debug(f'JSON data:\n{json.dumps(data, indent=2, separators = (",", ": "))}') # LANG: INARA API returned some kind of error (error message will be contained in {MSG}) - plug.show_error(_('Error: Inara {MSG}').format(MSG=error_message)) + plug.show_error(tr.tl('Error: Inara {MSG}').format(MSG=error_message)) def handle_success_reply(data: Mapping[str, Any], reply: dict[str, Any]) -> None: @@ -1645,7 +1679,7 @@ def handle_individual_error(data_event: dict[str, Any], reply_status: int, reply if reply_status // 100 != 2: # LANG: INARA API returned some kind of error (error message will be contained in {MSG}) - plug.show_error(_('Error: Inara {MSG}').format( + plug.show_error(tr.tl('Error: Inara {MSG}').format( MSG=f'{data_event["eventName"]}, {reply_text}' )) @@ -1666,7 +1700,7 @@ def handle_special_events(data_event: dict[str, Any], reply_event: dict[str, Any this.lastlocation = reply_event.get('eventData', {}) if not config.shutting_down: this.system_link.event_generate('<>', when="tail") - elif data_event['eventName'] in ['addCommanderShip', 'setCommanderShip']: + elif data_event['eventName'] in ('addCommanderShip', 'setCommanderShip'): this.lastship = reply_event.get('eventData', {}) if not config.shutting_down: this.system_link.event_generate('<>', when="tail") diff --git a/prefs.py b/prefs.py index 6c2722b6..95f29e7f 100644 --- a/prefs.py +++ b/prefs.py @@ -4,33 +4,28 @@ from __future__ import annotations import contextlib import logging -import pathlib +from os.path import expandvars, join, normpath +from pathlib import Path +import subprocess import sys -import tempfile import tkinter as tk -import webbrowser +import warnings from os import system -from os.path import expanduser, expandvars, join, normpath from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812 from tkinter import ttk from types import TracebackType -from typing import TYPE_CHECKING, Any, Callable, Optional, Type - +from typing import Any, Callable, Optional, Type import myNotebook as nb # noqa: N813 import plug -from config import applongname, appversion_nobuild, config +from config import appversion_nobuild, config from EDMCLogging import edmclogger, get_main_logger -from constants import appname from hotkey import hotkeymgr -from l10n import Translations +from l10n import translations as tr from monitor import monitor from theme import theme from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: - return x # TODO: Decouple this from platform as far as possible @@ -41,20 +36,36 @@ if TYPE_CHECKING: # May be imported by plugins - +# DEPRECATED: Migrate to open_log_folder. Will remove in 6.0 or later. def help_open_log_folder() -> None: """Open the folder logs are stored in.""" - logfile_loc = pathlib.Path(tempfile.gettempdir()) - logfile_loc /= f'{appname}' + warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. ' + 'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) + open_folder(Path(config.app_dir_path / 'logs')) + + +def open_folder(file: Path) -> None: + """Open the given file in the OS file explorer.""" if sys.platform.startswith('win'): # On Windows, use the "start" command to open the folder - system(f'start "" "{logfile_loc}"') - elif sys.platform.startswith('darwin'): - # On macOS, use the "open" command to open the folder - system(f'open "{logfile_loc}"') + system(f'start "" "{file}"') elif sys.platform.startswith('linux'): # On Linux, use the "xdg-open" command to open the folder - system(f'xdg-open "{logfile_loc}"') + system(f'xdg-open "{file}"') + + +def help_open_system_profiler(parent) -> None: + """Open the EDMC System Profiler.""" + profiler_path = config.respath_path + try: + if getattr(sys, 'frozen', False): + profiler_path /= 'EDMCSystemProfiler.exe' + subprocess.run(profiler_path, check=True) + else: + subprocess.run(['python', "EDMCSystemProfiler.py"], shell=True, check=True) + except Exception as err: + parent.status["text"] = tr.tl("Error in System Profiler") # LANG: Catch & Record Profiler Errors + logger.exception(err) class PrefsVersion: @@ -172,35 +183,12 @@ class AutoInc(contextlib.AbstractContextManager): return None -if sys.platform == 'darwin': - import objc # type: ignore - from Foundation import NSFileManager # type: ignore - try: - from ApplicationServices import ( # type: ignore - AXIsProcessTrusted, AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt - ) - - except ImportError: - HIServices = objc.loadBundle( - 'HIServices', - globals(), - '/System/Library/Frameworks/ApplicationServices.framework/Frameworks/HIServices.framework' - ) - - objc.loadBundleFunctions( - HIServices, - globals(), - [('AXIsProcessTrusted', 'B'), ('AXIsProcessTrustedWithOptions', 'B@')] - ) - - objc.loadBundleVariables(HIServices, globals(), [('kAXTrustedCheckOptionPrompt', '@^{__CFString=}')]) - - was_accessible_at_launch = AXIsProcessTrusted() # type: ignore - -elif sys.platform == 'win32': +if sys.platform == 'win32': import ctypes import winreg - from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL + import win32gui + import win32api is_wine = False try: WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine' @@ -215,6 +203,8 @@ elif sys.platform == 'win32': if not is_wine: try: CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition + CalculatePopupWindowPosition.argtypes = [POINT, SIZE, UINT, RECT, RECT] + CalculatePopupWindowPosition.restype = BOOL except AttributeError as e: logger.error( @@ -231,60 +221,49 @@ elif sys.platform == 'win32': ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] - SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)] - LoadString = ctypes.windll.user32.LoadStringW - LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int] - class PreferencesDialog(tk.Toplevel): """The EDMC preferences dialog.""" def __init__(self, parent: tk.Tk, callback: Optional[Callable]): - tk.Toplevel.__init__(self, parent) + super().__init__(parent) self.parent = parent self.callback = callback - if sys.platform == 'darwin': - # LANG: File > Preferences menu entry for macOS - self.title(_('Preferences')) - - else: - # LANG: File > Settings (macOS) - self.title(_('Settings')) + self.req_restart = False + # LANG: File > Settings (macOS) + self.title(tr.tl('Settings')) if parent.winfo_viewable(): self.transient(parent) - # position over parent - if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 - # TODO this is fixed supposedly. - self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}') + # Position over parent + self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}') - # remove decoration + # Remove decoration if sys.platform == 'win32': self.attributes('-toolwindow', tk.TRUE) - elif sys.platform == 'darwin': - # http://wiki.tcl.tk/13428 - parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') - - self.resizable(tk.FALSE, tk.FALSE) + # Allow the window to be resizable + self.resizable(tk.TRUE, tk.TRUE) self.cmdr: str | bool | None = False # Note if Cmdr changes in the Journal self.is_beta: bool = False # Note if Beta status changes in the Journal self.cmdrchanged_alarm: Optional[str] = None # This stores an ID that can be used to cancel a scheduled call + # Set up the main frame frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=0) - notebook: ttk.Notebook = nb.Notebook(frame) + notebook: nb.Notebook = nb.Notebook(frame) notebook.bind('<>', self.tabchanged) # Recompute on tab change self.PADX = 10 @@ -292,29 +271,26 @@ class PreferencesDialog(tk.Toplevel): self.LISTX = 25 # indent listed items self.PADY = 1 # close spacing self.BOXY = 2 # box spacing - self.SEPY = 10 # seperator line spacing + self.SEPY = 10 # separator line spacing # Set up different tabs - self.__setup_output_tab(notebook) - self.__setup_plugin_tabs(notebook) self.__setup_config_tab(notebook) - self.__setup_privacy_tab(notebook) self.__setup_appearance_tab(notebook) + self.__setup_output_tab(notebook) + self.__setup_privacy_tab(notebook) self.__setup_plugin_tab(notebook) + self.__setup_plugin_tabs(notebook) - if sys.platform == 'darwin': - self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes - - else: - buttonframe = ttk.Frame(frame) - 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("", lambda event: self.apply()) - self.protocol("WM_DELETE_WINDOW", self._destroy) + # Set up the button frame + buttonframe = ttk.Frame(frame) + 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=tr.tl('OK'), command=self.apply) + button.grid(row=0, column=1, sticky=tk.E) + button.bind("", lambda event: self.apply()) + self.protocol("WM_DELETE_WINDOW", self._destroy) # FIXME: Why are these being called when *creating* the Settings window? # Selectively disable buttons depending on output settings @@ -326,14 +302,14 @@ class PreferencesDialog(tk.Toplevel): # wait for window to appear on screen before calling grab_set self.parent.update_idletasks() - self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear ontop of parent on OSX & Linux + self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear on top of parent on Linux self.wait_visibility() self.grab_set() # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), SIZE(position.right - position.left, position.bottom - position.top), # type: ignore @@ -341,6 +317,15 @@ class PreferencesDialog(tk.Toplevel): ): self.geometry(f"+{position.left}+{position.top}") + # Set Log Directory + self.logfile_loc = Path(config.app_dir_path / 'logs') + + # Set minimum size to prevent content cut-off + self.update_idletasks() # Update "requested size" from geometry manager + min_width = self.winfo_reqwidth() + min_height = self.winfo_reqheight() + self.wm_minsize(min_width, min_height) + def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: output_frame = nb.Frame(root_notebook) output_frame.columnconfigure(0, weight=1) @@ -354,13 +339,13 @@ class PreferencesDialog(tk.Toplevel): row = AutoInc(start=0) # LANG: Settings > Output - choosing what data to save to files - self.out_label = nb.Label(output_frame, text=_('Please choose what data to save')) + self.out_label = nb.Label(output_frame, text=tr.tl('Please choose what data to save')) self.out_label.grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.out_csv = tk.IntVar(value=1 if (output & config.OUT_MKT_CSV) else 0) self.out_csv_button = nb.Checkbutton( output_frame, - text=_('Market data in CSV format file'), # LANG: Settings > Output option + text=tr.tl('Market data in CSV format file'), # LANG: Settings > Output option variable=self.out_csv, command=self.outvarchanged ) @@ -369,7 +354,7 @@ class PreferencesDialog(tk.Toplevel): self.out_td = tk.IntVar(value=1 if (output & config.OUT_MKT_TD) else 0) self.out_td_button = nb.Checkbutton( output_frame, - text=_('Market data in Trade Dangerous format file'), # LANG: Settings > Output option + text=tr.tl('Market data in Trade Dangerous format file'), # LANG: Settings > Output option variable=self.out_td, command=self.outvarchanged ) @@ -379,7 +364,7 @@ class PreferencesDialog(tk.Toplevel): # Output setting self.out_ship_button = nb.Checkbutton( output_frame, - text=_('Ship loadout'), # LANG: Settings > Output option + text=tr.tl('Ship loadout'), # LANG: Settings > Output option variable=self.out_ship, command=self.outvarchanged ) @@ -389,7 +374,7 @@ class PreferencesDialog(tk.Toplevel): # Output setting self.out_auto_button = nb.Checkbutton( output_frame, - text=_('Automatically update on docking'), # LANG: Settings > Output option + text=tr.tl('Automatically update on docking'), # LANG: Settings > Output option variable=self.out_auto, command=self.outvarchanged ) @@ -398,33 +383,27 @@ class PreferencesDialog(tk.Toplevel): self.outdir = tk.StringVar() self.outdir.set(str(config.get_str('outdir'))) # LANG: Settings > Output - Label for "where files are located" - self.outdir_label = nb.Label(output_frame, text=_('File location')+':') # Section heading in settings + self.outdir_label = nb.Label(output_frame, text=tr.tl('File location')+':') # Section heading in settings # Type ignored due to incorrect type annotation. a 2 tuple does padding for each side self.outdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) # type: ignore - self.outdir_entry = nb.Entry(output_frame, takefocus=False) + self.outdir_entry = ttk.Entry(output_frame, takefocus=False) self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) - if sys.platform == 'darwin': - text = (_('Change...')) # LANG: macOS Preferences - files location selection button + text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button - else: - text = (_('Browse...')) # LANG: NOT-macOS Settings - files location selection button - - self.outbutton = nb.Button( + self.outbutton = ttk.Button( output_frame, text=text, # Technically this is different from the label in Settings > Output, as *this* is used # as the title of the popup folder selection window. # LANG: Settings > Output - Label for "where files are located" - command=lambda: self.filebrowse(_('File location'), self.outdir) + command=lambda: self.filebrowse(tr.tl('File location'), self.outdir) ) self.outbutton.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=row.get()) - nb.Frame(output_frame).grid(row=row.get()) # bottom spacer # TODO: does nothing? - # LANG: Label for 'Output' Settings/Preferences tab - root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings + root_notebook.add(output_frame, text=tr.tl('Output')) # Tab heading in settings def __setup_plugin_tabs(self, notebook: ttk.Notebook) -> None: for plugin in plug.PLUGINS: @@ -444,38 +423,34 @@ class PreferencesDialog(tk.Toplevel): logdir = default self.logdir.set(logdir) - self.logdir_entry = nb.Entry(config_frame, takefocus=False) + self.logdir_entry = ttk.Entry(config_frame, takefocus=False) # Location of the Journal files nb.Label( config_frame, # LANG: Settings > Configuration - Label for Journal files location - text=_('E:D journal file location')+':' + text=tr.tl('E:D journal file location')+':' ).grid(columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) - if sys.platform == 'darwin': - text = (_('Change...')) # LANG: macOS Preferences - files location selection button - - else: - text = (_('Browse...')) # LANG: NOT-macOS Setting - files location selection button + text = tr.tl('Browse...') # LANG: NOT-macOS Setting - files location selection button with row as cur_row: - self.logbutton = nb.Button( + self.logbutton = ttk.Button( config_frame, text=text, # LANG: Settings > Configuration - Label for Journal files location - command=lambda: self.filebrowse(_('E:D journal file location'), self.logdir) + command=lambda: self.filebrowse(tr.tl('E:D journal file location'), self.logdir) ) self.logbutton.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) if config.default_journal_dir_path: # Appearance theme and language setting - nb.Button( + ttk.Button( config_frame, # LANG: Settings > Configuration - Label on 'reset journal files location to default' button - text=_('Default'), + text=tr.tl('Default'), command=self.logdir_reset, state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED ).grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) @@ -489,17 +464,17 @@ class PreferencesDialog(tk.Toplevel): nb.Label( config_frame, - text=_('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section + text=tr.tl('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) nb.Checkbutton( config_frame, # LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls - text=_('Enable Fleetcarrier CAPI Queries'), + text=tr.tl('Enable Fleet Carrier CAPI Queries'), variable=self.capi_fleetcarrier ).grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) - if sys.platform in ('darwin', 'win32'): + if sys.platform == 'win32': ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid( columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) @@ -511,55 +486,27 @@ class PreferencesDialog(tk.Toplevel): with row as cur_row: nb.Label( config_frame, - text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX - sys.platform == 'darwin' else - _('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows + text=tr.tl('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) - if sys.platform == 'darwin' and not was_accessible_at_launch: - if AXIsProcessTrusted(): - # Shortcut settings prompt on OSX - nb.Label( - config_frame, - # LANG: macOS Preferences > Configuration - restart the app message - text=_('Re-start {APP} to use shortcuts').format(APP=applongname), - foreground='firebrick' - ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + self.hotkey_text = ttk.Entry(config_frame, width=30, justify=tk.CENTER) + self.hotkey_text.insert( + 0, + # No hotkey/shortcut currently defined + # TODO: display Only shows up on windows + # LANG: No hotkey/shortcut set + hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None') + ) - else: - # Shortcut settings prompt on OSX - nb.Label( - config_frame, - # LANG: macOS - Configuration - need to grant the app permission for keyboard shortcuts - text=_('{APP} needs permission to use shortcuts').format(APP=applongname), - foreground='firebrick' - ).grid(columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) - - # LANG: Shortcut settings button on OSX - nb.Button(config_frame, text=_('Open System Preferences'), command=self.enableshortcuts).grid( - padx=self.PADX, pady=self.BOXY, sticky=tk.E, row=cur_row - ) - - else: - self.hotkey_text = nb.Entry(config_frame, width=( - 20 if sys.platform == 'darwin' else 30), justify=tk.CENTER) - self.hotkey_text.insert( - 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') - ) - - self.hotkey_text.bind('', self.hotkeystart) - self.hotkey_text.bind('', self.hotkeyend) - self.hotkey_text.grid(column=1, columnspan=2, pady=self.BOXY, sticky=tk.W, row=cur_row) + self.hotkey_text.bind('', self.hotkeystart) + self.hotkey_text.bind('', self.hotkeyend) + self.hotkey_text.grid(column=1, columnspan=2, pady=self.BOXY, sticky=tk.W, row=cur_row) # Hotkey/Shortcut setting self.hotkey_only_btn = nb.Checkbutton( config_frame, # LANG: Configuration - Act on hotkey only when ED is in foreground - text=_('Only when Elite: Dangerous is the active app'), + text=tr.tl('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state=tk.NORMAL if self.hotkey_code else tk.DISABLED ) @@ -570,22 +517,41 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_play_btn = nb.Checkbutton( config_frame, # LANG: Configuration - play sound when hotkey used - text=_('Play sound'), + text=tr.tl('Play sound'), variable=self.hotkey_play, state=tk.NORMAL if self.hotkey_code else tk.DISABLED ) self.hotkey_play_btn.grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) - # Option to disabled Automatic Check For Updates whilst in-game + # Options to select the Update Path and Disable Automatic Checks For Updates whilst in-game ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid( columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) + + with row as curr_row: + nb.Label(config_frame, text=tr.tl('Update Track')).grid( # LANG: Select the Update Track (Beta, Stable) + padx=self.PADX, pady=self.PADY, sticky=tk.W, row=curr_row + ) + self.curr_update_track = "Beta" if config.get_bool('beta_optin') else "Stable" + self.update_paths = tk.StringVar(value=self.curr_update_track) + + update_paths = [ + tr.tl("Stable"), # LANG: Stable Version of EDMC + tr.tl("Beta") # LANG: Beta Version of EDMC + ] + self.update_track = nb.OptionMenu( + config_frame, self.update_paths, self.update_paths.get(), *update_paths + ) + + self.update_track.configure(width=15) + self.update_track.grid(column=1, pady=self.BOXY, padx=self.PADX, sticky=tk.W, row=curr_row) + self.disable_autoappupdatecheckingame = tk.IntVar(value=config.get_int('disable_autoappupdatecheckingame')) self.disable_autoappupdatecheckingame_btn = nb.Checkbutton( config_frame, # LANG: Configuration - disable checks for app updates when in-game - text=_('Disable Automatic Application Updates Check when in-game'), + text=tr.tl('Disable Automatic Application Updates Check when in-game'), variable=self.disable_autoappupdatecheckingame, command=self.disable_autoappupdatecheckingame_changed ) @@ -600,7 +566,7 @@ class PreferencesDialog(tk.Toplevel): # Settings prompt for preferred ship loadout, system and station info websites # LANG: Label for preferred shipyard, system and station 'providers' - nb.Label(config_frame, text=_('Preferred websites')).grid( + nb.Label(config_frame, text=tr.tl('Preferred websites')).grid( columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -611,7 +577,9 @@ class PreferencesDialog(tk.Toplevel): ) # Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis # LANG: Label for Shipyard provider selection - nb.Label(config_frame, text=_('Shipyard')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('Shipyard')).grid( + padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row + ) self.shipyard_button = nb.OptionMenu( config_frame, self.shipyard_provider, self.shipyard_provider.get(), *plug.provides('shipyard_url') ) @@ -623,7 +591,7 @@ class PreferencesDialog(tk.Toplevel): self.alt_shipyard_open_btn = nb.Checkbutton( config_frame, # LANG: Label for checkbox to utilise alternative Coriolis URL method - text=_('Use alternate URL method'), + text=tr.tl('Use alternate URL method'), variable=self.alt_shipyard_open, command=self.alt_shipyard_open_changed, ) @@ -637,7 +605,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Configuration - Label for selection of 'System' provider website - nb.Label(config_frame, text=_('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) self.system_button = nb.OptionMenu( config_frame, self.system_provider, @@ -655,7 +623,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Configuration - Label for selection of 'Station' provider website - nb.Label(config_frame, text=_('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + nb.Label(config_frame, text=tr.tl('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) self.station_button = nb.OptionMenu( config_frame, self.station_provider, @@ -676,7 +644,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( config_frame, # LANG: Configuration - Label for selection of Log Level - text=_('Log Level') + text=tr.tl('Log Level') ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) current_loglevel = config.get_str('loglevel') @@ -700,18 +668,18 @@ class PreferencesDialog(tk.Toplevel): self.loglevel_dropdown.configure(width=15) self.loglevel_dropdown.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row) - nb.Button( + ttk.Button( config_frame, # LANG: Label on button used to open a filesystem folder - text=_('Open Log Folder'), # Button that opens a folder in Explorer/Finder - command=lambda: help_open_log_folder() + text=tr.tl('Open Log Folder'), # Button that opens a folder in Explorer/Finder + command=lambda: open_folder(self.logfile_loc) ).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row) # Big spacer nb.Label(config_frame).grid(sticky=tk.W, row=row.get()) # LANG: Label for 'Configuration' tab in Settings - notebook.add(config_frame, text=_('Configuration')) + notebook.add(config_frame, text=tr.tl('Configuration')) def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None: privacy_frame = nb.Frame(notebook) @@ -720,37 +688,37 @@ class PreferencesDialog(tk.Toplevel): row = AutoInc(start=0) # LANG: UI elements privacy section header in privacy tab of preferences - nb.Label(privacy_frame, text=_('Main UI privacy options')).grid( + nb.Label(privacy_frame, text=tr.tl('Main UI privacy options')).grid( row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=self.PADY ) nb.Checkbutton( # LANG: Hide private group owner name from UI checkbox - privacy_frame, text=_('Hide private group name in UI'), + privacy_frame, text=tr.tl('Hide private group name in UI'), variable=self.hide_private_group ).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W) nb.Checkbutton( # LANG: Hide multicrew captain name from main UI checkbox - privacy_frame, text=_('Hide multi-crew captain name'), + privacy_frame, text=tr.tl('Hide multi-crew captain name'), variable=self.hide_multicrew_captain ).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W) - notebook.add(privacy_frame, text=_('Privacy')) # LANG: Preferences privacy tab title + notebook.add(privacy_frame, text=tr.tl('Privacy')) # LANG: Preferences privacy tab title def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: - self.languages = Translations.available_names() + self.languages = tr.available_names() # Appearance theme and language setting # LANG: The system default language choice in Settings > Appearance - self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), _('Default'))) + self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), tr.tl('Default'))) self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) self.minimize_system_tray = tk.BooleanVar(value=config.get_bool('minimize_system_tray')) self.theme = tk.IntVar(value=config.get_int('theme')) self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')] self.theme_prompts = [ # LANG: Label for Settings > Appeareance > selection of 'normal' text colour - _('Normal text'), # Dark theme color setting + tr.tl('Normal text'), # Dark theme color setting # LANG: Label for Settings > Appeareance > selection of 'highlightes' text colour - _('Highlighted text'), # Dark theme color setting + tr.tl('Highlighted text'), # Dark theme color setting ] row = AutoInc(start=0) @@ -759,7 +727,7 @@ class PreferencesDialog(tk.Toplevel): appearance_frame.columnconfigure(2, weight=1) with row as cur_row: # LANG: Appearance - Label for selection of application display language - nb.Label(appearance_frame, text=_('Language')).grid( + nb.Label(appearance_frame, text=tr.tl('Language')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) self.lang_button = nb.OptionMenu(appearance_frame, self.lang, self.lang.get(), *self.languages.values()) @@ -771,28 +739,29 @@ class PreferencesDialog(tk.Toplevel): # Appearance setting # LANG: Label for Settings > Appearance > Theme selection - nb.Label(appearance_frame, text=_('Theme')).grid( + nb.Label(appearance_frame, text=tr.tl('Theme')).grid( columnspan=3, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) # Appearance theme and language setting nb.Radiobutton( # LANG: Label for 'Default' theme radio button - appearance_frame, text=_('Default'), variable=self.theme, + appearance_frame, text=tr.tl('Default'), variable=self.theme, value=theme.THEME_DEFAULT, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance theme setting nb.Radiobutton( # LANG: Label for 'Dark' theme radio button - appearance_frame, text=_('Dark'), variable=self.theme, value=theme.THEME_DARK, command=self.themevarchanged + appearance_frame, text=tr.tl('Dark'), variable=self.theme, + value=theme.THEME_DARK, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) if sys.platform == 'win32': nb.Radiobutton( appearance_frame, # LANG: Label for 'Transparent' theme radio button - text=_('Transparent'), # Appearance theme setting + text=tr.tl('Transparent'), # Appearance theme setting variable=self.theme, value=theme.THEME_TRANSPARENT, command=self.themevarchanged @@ -803,10 +772,10 @@ class PreferencesDialog(tk.Toplevel): self.theme_label_0.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) # Main window - self.theme_button_0 = nb.ColoredButton( + self.theme_button_0 = tk.Button( appearance_frame, # LANG: Appearance - Example 'Normal' text - text=_('Station'), + text=tr.tl('Station'), background='grey4', command=lambda: self.themecolorbrowse(0) ) @@ -816,7 +785,7 @@ class PreferencesDialog(tk.Toplevel): with row as cur_row: self.theme_label_1 = nb.Label(appearance_frame, text=self.theme_prompts[1]) self.theme_label_1.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) - self.theme_button_1 = nb.ColoredButton( + self.theme_button_1 = tk.Button( appearance_frame, text=' Hutton Orbital ', # Do not translate background='grey4', @@ -838,7 +807,7 @@ class PreferencesDialog(tk.Toplevel): ) with row as cur_row: # LANG: Appearance - Label for selection of UI scaling - nb.Label(appearance_frame, text=_('UI Scale Percentage')).grid( + nb.Label(appearance_frame, text=tr.tl('UI Scale Percentage')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) @@ -859,7 +828,7 @@ class PreferencesDialog(tk.Toplevel): self.ui_scaling_defaultis = nb.Label( appearance_frame, # LANG: Appearance - Help/hint text for UI scaling selection - text=_('100 means Default{CR}Restart Required for{CR}changes to take effect!') + text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!') ).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row) # Transparency slider @@ -869,7 +838,7 @@ class PreferencesDialog(tk.Toplevel): with row as cur_row: # LANG: Appearance - Label for selection of main window transparency - nb.Label(appearance_frame, text=_("Main window transparency")).grid( + nb.Label(appearance_frame, text=tr.tl("Main window transparency")).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) self.transparency = tk.IntVar() @@ -889,7 +858,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label( appearance_frame, # LANG: Appearance - Help/hint text for Main window transparency selection - text=_( + text=tr.tl( "100 means fully opaque.{CR}" "Window is updated in real time" ).format(CR='\n') @@ -911,7 +880,7 @@ class PreferencesDialog(tk.Toplevel): self.ontop_button = nb.Checkbutton( appearance_frame, # LANG: Appearance - Label for checkbox to select if application always on top - text=_('Always on top'), + text=tr.tl('Always on top'), variable=self.always_ontop, command=self.themevarchanged ) @@ -923,7 +892,7 @@ class PreferencesDialog(tk.Toplevel): nb.Checkbutton( appearance_frame, # LANG: Appearance option for Windows "minimize to system tray" - text=_('Minimize to system tray'), + text=tr.tl('Minimize to system tray'), variable=self.minimize_system_tray, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance setting @@ -931,50 +900,68 @@ class PreferencesDialog(tk.Toplevel): nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer # LANG: Label for Settings > Appearance tab - notebook.add(appearance_frame, text=_('Appearance')) # Tab heading in settings + notebook.add(appearance_frame, text=tr.tl('Appearance')) # Tab heading in settings def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 # Plugin settings and info plugins_frame = nb.Frame(notebook) plugins_frame.columnconfigure(0, weight=1) - plugdir = tk.StringVar() - plugdir.set(config.plugin_dir) row = AutoInc(start=0) - - # Section heading in settings + self.plugdir = tk.StringVar() + self.plugdir.set(str(config.get_str('plugin_dir'))) # LANG: Label for location of third-party plugins folder - nb.Label(plugins_frame, text=_('Plugins folder') + ':').grid( - padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() - ) - - plugdirentry = nb.Entry(plugins_frame, justify=tk.LEFT) - self.displaypath(plugdir, plugdirentry) - plugdirentry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) - + self.plugdir_label = nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':') + self.plugdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) + self.plugdir_entry = ttk.Entry(plugins_frame, takefocus=False, + textvariable=self.plugdir) # Link StringVar to Entry widget + self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) with row as cur_row: 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=self.PADY, sticky=tk.EW, row=cur_row) + text=tr.tl( + "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled') + ).grid(columnspan=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) - nb.Button( + # Open Plugin Folder Button + self.open_plug_folder_btn = ttk.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=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row) + # LANG: Label on button used to open the Plugin Folder + text=tr.tl('Open Plugins Folder'), + command=lambda: open_folder(config.plugin_dir_path) + ) + self.open_plug_folder_btn.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) + + # Browse Button + text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button + self.plugbutton = ttk.Button( + plugins_frame, + text=text, + # LANG: Selecting the Location of the Plugin Directory on the Filesystem + command=lambda: self.filebrowse(tr.tl('Plugin Directory Location'), self.plugdir) + ) + self.plugbutton.grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) + + if config.default_journal_dir_path: + # Appearance theme and language setting + ttk.Button( + plugins_frame, + # LANG: Settings > Configuration - Label on 'reset journal files location to default' button + text=tr.tl('Default'), + command=self.plugdir_reset, + state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED + ).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row) enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS)) - if len(enabled_plugins): + if enabled_plugins: ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( - columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() + columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) nb.Label( plugins_frame, # LANG: Label on list of enabled plugins - text=_('Enabled Plugins')+':' # List of plugins in settings + text=tr.tl('Enabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in enabled_plugins: @@ -989,18 +976,18 @@ class PreferencesDialog(tk.Toplevel): ############################################################ # Show which plugins don't have Python 3.x support ############################################################ - if len(plug.PLUGINS_not_py3): + if plug.PLUGINS_not_py3: ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x - nb.Label(plugins_frame, text=_('Plugins Without Python 3.x Support')+':').grid( + nb.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) HyperlinkLabel( # LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7 - plugins_frame, text=_('Information on migrating plugins'), + plugins_frame, text=tr.tl('Information on migrating plugins'), background=nb.Label().cget('background'), url='https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#migration-from-python-27', underline=True @@ -1015,14 +1002,14 @@ class PreferencesDialog(tk.Toplevel): # Show disabled plugins ############################################################ disabled_plugins = list(filter(lambda x: x.folder and not x.module, plug.PLUGINS)) - if len(disabled_plugins): + if disabled_plugins: ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) nb.Label( plugins_frame, # LANG: Label on list of user-disabled plugins - text=_('Disabled Plugins')+':' # List of plugins in settings + text=tr.tl('Disabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in disabled_plugins: @@ -1032,12 +1019,12 @@ class PreferencesDialog(tk.Toplevel): ############################################################ # Show plugins that failed to load ############################################################ - if len(plug.PLUGINS_broken): + if plug.PLUGINS_broken: ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'broken' plugins that failed to load - nb.Label(plugins_frame, text=_('Broken Plugins')+':').grid( + nb.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -1048,7 +1035,7 @@ class PreferencesDialog(tk.Toplevel): ) # LANG: Label on Settings > Plugins tab - notebook.add(plugins_frame, text=_('Plugins')) # Tab heading in settings + notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings def cmdrchanged(self, event=None): """ @@ -1070,14 +1057,6 @@ class PreferencesDialog(tk.Toplevel): def tabchanged(self, event: tk.Event) -> None: """Handle preferences active tab changing.""" self.outvarchanged() - if sys.platform == 'darwin': - # Hack to recompute size so that buttons show up under Mojave - notebook = event.widget - frame = self.nametowidget(notebook.winfo_parent()) - temp = nb.Label(frame) - temp.grid() - temp.update_idletasks() - temp.destroy() def outvarchanged(self, event: Optional[tk.Event] = None) -> None: """Handle Output tab variable changes.""" @@ -1099,7 +1078,7 @@ class PreferencesDialog(tk.Toplevel): import tkinter.filedialog directory = tkinter.filedialog.askdirectory( parent=self, - initialdir=expanduser(pathvar.get()), + initialdir=Path(pathvar.get()).expanduser(), title=title, mustexist=tk.TRUE ) @@ -1127,7 +1106,8 @@ class PreferencesDialog(tk.Toplevel): for i in range(start, len(components)): try: if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and - LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)): + win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, + pidsRes.value, buf, MAX_PATH)): display.append(buf.value) else: @@ -1139,16 +1119,6 @@ class PreferencesDialog(tk.Toplevel): entryfield.insert(0, '\\'.join(display)) # None if path doesn't exist - elif sys.platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()): - if pathvar.get().startswith(config.home): - display = ['~'] + NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())[ - len(NSFileManager.defaultManager().componentsToDisplayForPath_(config.home)): - ] - - else: - display = NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()) - - entryfield.insert(0, '/'.join(display)) else: if pathvar.get().startswith(config.home): entryfield.insert(0, '~' + pathvar.get()[len(config.home):]) @@ -1165,6 +1135,13 @@ class PreferencesDialog(tk.Toplevel): self.outvarchanged() + def plugdir_reset(self) -> None: + """Reset the log dir to the default.""" + if config.default_plugin_dir_path: + self.plugdir.set(config.default_plugin_dir) + + self.outvarchanged() + def disable_autoappupdatecheckingame_changed(self) -> None: """Save out the auto update check in game config.""" config.set('disable_autoappupdatecheckingame', self.disable_autoappupdatecheckingame.get()) @@ -1219,7 +1196,7 @@ class PreferencesDialog(tk.Toplevel): self.hotkey_text.insert( 0, # LANG: No hotkey/shortcut set - hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')) + hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None')) def hotkeylisten(self, event: 'tk.Event[Any]') -> str: """ @@ -1252,7 +1229,7 @@ class PreferencesDialog(tk.Toplevel): else: # LANG: No hotkey/shortcut set - event.widget.insert(0, _('None')) + event.widget.insert(0, tr.tl('None')) self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED @@ -1260,7 +1237,7 @@ class PreferencesDialog(tk.Toplevel): return 'break' # stops further processing - insertion, Tab traversal etc - def apply(self) -> None: + def apply(self) -> None: # noqa: CCR001 """Update the config with the options set on the dialog.""" config.set('PrefsVersion', prefsVersion.stringToSerial(appversion_nobuild())) config.set( @@ -1288,12 +1265,12 @@ class PreferencesDialog(tk.Toplevel): config.set('capi_fleetcarrier', self.capi_fleetcarrier.get()) - if sys.platform in ('darwin', 'win32'): + if sys.platform == 'win32': config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) - + config.set('beta_optin', 0 if self.update_paths.get() == "Stable" else 1) config.set('shipyard_provider', self.shipyard_provider.get()) config.set('system_provider', self.system_provider.get()) config.set('station_provider', self.station_provider.get()) @@ -1302,7 +1279,7 @@ class PreferencesDialog(tk.Toplevel): lang_codes = {v: k for k, v in self.languages.items()} # Codes by name config.set('language', lang_codes.get(self.lang.get()) or '') # or '' used here due to Default being None above - Translations.install(config.get_str('language', default=None)) # type: ignore # This sets self in weird ways. + tr.install(config.get_str('language', default=None)) # type: ignore # This sets self in weird ways. # Privacy options config.set('hide_private_group', self.hide_private_group.get()) @@ -1316,6 +1293,13 @@ class PreferencesDialog(tk.Toplevel): config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) theme.apply(self.parent) + if self.plugdir.get() != config.get('plugin_dir'): + config.set( + 'plugin_dir', + join(config.home_path, self.plugdir.get()[2:]) if self.plugdir.get().startswith( + '~') else self.plugdir.get() + ) + self.req_restart = True # Notify if self.callback: @@ -1324,6 +1308,15 @@ class PreferencesDialog(tk.Toplevel): plug.notify_prefs_changed(monitor.cmdr, monitor.is_beta) self._destroy() + # Send to the Post Config if we updated the update branch or need to restart + post_flags = { + 'Update': True if self.curr_update_track != self.update_paths.get() else False, + 'Track': self.update_paths.get(), + 'Parent': self, + 'Restart_Req': True if self.req_restart else False + } + if self.callback: + self.callback(**post_flags) def _destroy(self) -> None: """widget.destroy wrapper that does some extra cleanup.""" @@ -1333,25 +1326,3 @@ class PreferencesDialog(tk.Toplevel): self.parent.wm_attributes('-topmost', 1 if config.get_int('always_ontop') else 0) self.destroy() - - if sys.platform == 'darwin': - def enableshortcuts(self) -> None: - """Set up macOS preferences shortcut.""" - self.apply() - # popup System Preferences dialog - try: - # http://stackoverflow.com/questions/6652598/cocoa-button-opens-a-system-preference-page/6658201 - from ScriptingBridge import SBApplication # type: ignore - sysprefs = 'com.apple.systempreferences' - prefs = SBApplication.applicationWithBundleIdentifier_(sysprefs) - pane = [x for x in prefs.panes() if x.id() == 'com.apple.preference.security'][0] - prefs.setCurrentPane_(pane) - anchor = [x for x in pane.anchors() if x.name() == 'Privacy_Accessibility'][0] - anchor.reveal() - prefs.activate() - - except Exception: - AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: True}) - - if not config.shutting_down: - self.parent.event_generate('<>', when="tail") diff --git a/protocol.py b/protocol.py index f3b956d2..01be09de 100644 --- a/protocol.py +++ b/protocol.py @@ -26,6 +26,7 @@ is_wine = False if sys.platform == 'win32': from ctypes import windll # type: ignore + try: if windll.ntdll.wine_get_version: is_wine = True @@ -58,72 +59,13 @@ class GenericProtocolHandler: self.master.event_generate('<>', when="tail") -if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # noqa: C901 # its guarding ALL macos stuff. - import struct - - import objc # type: ignore - from AppKit import NSAppleEventManager, NSObject # type: ignore - - kInternetEventClass = kAEGetURL = struct.unpack('>l', b'GURL')[0] # noqa: N816 # API names - keyDirectObject = struct.unpack('>l', b'----')[0] # noqa: N816 # API names - - class DarwinProtocolHandler(GenericProtocolHandler): - """ - MacOS protocol handler implementation. - - Uses macOS event stuff. - """ - - POLL = 100 # ms - - def start(self, master: 'tkinter.Tk') -> None: - """Start Protocol Handler.""" - GenericProtocolHandler.start(self, master) - self.lasturl: str | None = None - self.eventhandler = EventHandler.alloc().init() - - def poll(self) -> None: - """Poll event until URL is updated.""" - # No way of signalling to Tkinter from within the callback handler block that doesn't cause Python to crash, - # so poll. TODO: Resolved? - if self.lasturl and self.lasturl.startswith(self.redirect): - self.event(self.lasturl) - self.lasturl = None - - class EventHandler(NSObject): - """Handle NSAppleEventManager IPC stuff.""" - - def init(self) -> None: - """ - Init method for handler. - - (I'd assume this is related to the subclassing of NSObject for why its not __init__) - """ - self = objc.super(EventHandler, self).init() - NSAppleEventManager.sharedAppleEventManager().setEventHandler_andSelector_forEventClass_andEventID_( - self, - 'handleEvent:withReplyEvent:', - kInternetEventClass, - kAEGetURL - ) - return self - - def handleEvent_withReplyEvent_(self, event, replyEvent) -> None: # noqa: N802 N803 # Required to override - """Actual event handling from NSAppleEventManager.""" - protocolhandler.lasturl = parse.unquote( - event.paramDescriptorForKeyword_(keyDirectObject).stringValue() - ).strip() - - protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll) - - -elif (config.auth_force_edmc_protocol - or ( - sys.platform == 'win32' - and getattr(sys, 'frozen', False) - and not is_wine - and not config.auth_force_localserver - )): +if (config.auth_force_edmc_protocol # noqa: C901 + or ( + sys.platform == 'win32' + and getattr(sys, 'frozen', False) + and not is_wine + and not config.auth_force_localserver + )): # This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces assert sys.platform == 'win32' # spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL @@ -134,6 +76,7 @@ elif (config.auth_force_edmc_protocol ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, MSG, UINT, WPARAM ) + import win32gui class WNDCLASS(Structure): """ @@ -163,14 +106,6 @@ elif (config.auth_force_edmc_protocol CreateWindowExW.restype = HWND RegisterClassW = windll.user32.RegisterClassW RegisterClassW.argtypes = [POINTER(WNDCLASS)] - # DefWindowProcW - # Ref: - # LRESULT DefWindowProcW([in] HWND hWnd,[in] UINT Msg,[in] WPARAM wParam,[in] LPARAM lParam); - # As per example at - - prototype = WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM) - paramflags = (1, "hWnd"), (1, "Msg"), (1, "wParam"), (1, "lParam") - DefWindowProcW = prototype(("DefWindowProcW", windll.user32), paramflags) GetParent = windll.user32.GetParent SetForegroundWindow = windll.user32.SetForegroundWindow @@ -229,7 +164,7 @@ elif (config.auth_force_edmc_protocol if message != WM_DDE_INITIATE: # Not a DDE init message, bail and tell windows to do the default # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowproca?redirectedfrom=MSDN - return DefWindowProcW(hwnd, message, wParam, lParam) + return win32gui.DefWindowProc(hwnd, message, wParam, lParam) service = create_unicode_buffer(256) topic = create_unicode_buffer(256) @@ -245,11 +180,11 @@ elif (config.auth_force_edmc_protocol # which we can read out as shown below, and then compare. target_is_valid = lparam_low == 0 or ( - GlobalGetAtomNameW(lparam_low, service, 256) and service.value == appname + GlobalGetAtomNameW(lparam_low, service, 256) and service.value == appname ) topic_is_valid = lparam_high == 0 or ( - GlobalGetAtomNameW(lparam_high, topic, 256) and topic.value.lower() == 'system' + GlobalGetAtomNameW(lparam_high, topic, 256) and topic.value.lower() == 'system' ) if target_is_valid and topic_is_valid: @@ -441,7 +376,7 @@ else: # Linux / Run from source if self.parse(): self.send_header('Content-Type', 'text/html') self.end_headers() - self.wfile.write(self._generate_auth_response().encode('utf-8')) + self.wfile.write(self._generate_auth_response().encode()) else: self.send_response(404) self.end_headers() @@ -480,12 +415,9 @@ def get_handler_impl() -> Type[GenericProtocolHandler]: :return: An instantiatable GenericProtocolHandler """ - if sys.platform == 'darwin' and getattr(sys, 'frozen', False): - return DarwinProtocolHandler # pyright: reportUnboundVariable=false - if ( - (sys.platform == 'win32' and config.auth_force_edmc_protocol) - or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver) + (sys.platform == 'win32' and config.auth_force_edmc_protocol) + or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver) ): return WindowsProtocolHandler diff --git a/pyproject.toml b/pyproject.toml index e32ad617..b98fbf4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,3 @@ sys-platform-not-darwin = "sys_platform == 'darwin'" sys-platform-linux = "sys_platform != 'linux'" sys-platform-not-linux = "sys_platform == 'linux'" sys-platform-not-known = "sys_platform in ('darwin', 'linux', 'win32')" - -[tool.pyright] -# pythonPlatform = 'Darwin' diff --git a/requirements-dev.txt b/requirements-dev.txt index 7fb98e50..81e36d88 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,49 +1,43 @@ -# So that you don't get warnings like: -# Using legacy 'setup.py install' for flake8-annotations-coverage, since package 'wheel' is not installed. -wheel - # We can't rely on just picking this up from either the base (not venv), # or venv-init-time version. Specify here so that dependabot will prod us # about new versions. -setuptools==69.0.2 +setuptools==75.8.0 # Static analysis tools -flake8==6.1.0 +flake8==7.1.1 flake8-annotations-coverage==0.0.6 flake8-cognitive-complexity==0.1.0 -flake8-comprehensions==3.14.0 +flake8-comprehensions==3.15.0 flake8-docstrings==1.7.0 -flake8-json==23.7.0 -flake8-noqa==1.3.2 +flake8-json==24.4.0 +flake8-noqa==1.4.0 flake8-polyfill==1.0.2 flake8-use-fstring==1.4 -mypy==1.7.1 -pep8-naming==0.13.3 -safety==2.3.5 -types-requests==2.31.0.10 -types-pkg-resources==0.1.3 +mypy==1.14.1 +pep8-naming==0.14.1 +safety==3.2.14 +types-requests==2.32.0.20241016 +types-setuptools==75.8.0.20250210 # Code formatting tools -autopep8==2.0.4 +autopep8==2.3.2 # Git pre-commit checking -pre-commit==3.5.0 +pre-commit==3.8.0 # HTML changelogs -grip==4.6.2 +mistune==3.1.1 # Packaging # We only need py2exe on windows. -py2exe==0.13.0.1; sys_platform == 'win32' +py2exe==0.13.0.2; sys_platform == 'win32' # Testing -pytest==7.4.3 -pytest-cov==4.1.0 # Pytest code coverage support -coverage[toml]==7.3.2 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs +pytest==8.3.2 +pytest-cov==6.0.0 # Pytest code coverage support +coverage[toml]==7.6.1 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 -# For manipulating folder permissions and the like. -pywin32==306; sys_platform == 'win32' # All of the normal requirements diff --git a/requirements.txt b/requirements.txt index 398041a1..b4d02a4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,8 @@ -certifi==2023.11.17 -requests==2.31.0 -# requests depends on this now ? -charset-normalizer==2.1.1 - -watchdog==3.0.0 -# Commented out because this doesn't package well with py2exe -infi.systray==0.1.12; sys_platform == 'win32' -# argh==0.26.2 watchdog dep -# pyyaml==5.3.1 watchdog dep +requests==2.32.3 +pillow==11.1.0 +watchdog==6.0.0 +simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 - -# Base requirement for MacOS -pyobjc; sys_platform == 'darwin' +# For manipulating folder permissions and the like. +pywin32==308; sys_platform == 'win32' +psutil==6.1.1 diff --git a/EDMC.manifest b/resources/EDMC.manifest similarity index 100% rename from EDMC.manifest rename to resources/EDMC.manifest diff --git a/resources/EDMC_Installer_Config_template.txt b/resources/EDMC_Installer_Config_template.txt index b72ed507..de40630c 100644 --- a/resources/EDMC_Installer_Config_template.txt +++ b/resources/EDMC_Installer_Config_template.txt @@ -16,7 +16,7 @@ AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#SuppURL} AppUpdatesURL={#SuppURL} -AppCopyright=Copyright (C) 2015-2019 Jonathan Harris, 2020-2023 EDCD +AppCopyright=Copyright (C) 2015-2019 Jonathan Harris, 2020-2024 EDCD AllowUNCPath=no AllowNetworkDrive=no DefaultDirName={autopf}\{#MyAppName} diff --git a/EDMarketConnector-dpiaware+gdiscaling.manifest b/resources/EDMarketConnector-dpiaware+gdiscaling.manifest similarity index 100% rename from EDMarketConnector-dpiaware+gdiscaling.manifest rename to resources/EDMarketConnector-dpiaware+gdiscaling.manifest diff --git a/EDMarketConnector-dpiaware.manifest b/resources/EDMarketConnector-dpiaware.manifest similarity index 100% rename from EDMarketConnector-dpiaware.manifest rename to resources/EDMarketConnector-dpiaware.manifest diff --git a/EDMarketConnector-gdiscaling.manifest b/resources/EDMarketConnector-gdiscaling.manifest similarity index 100% rename from EDMarketConnector-gdiscaling.manifest rename to resources/EDMarketConnector-gdiscaling.manifest diff --git a/EDMarketConnector.manifest b/resources/EDMarketConnector.manifest similarity index 100% rename from EDMarketConnector.manifest rename to resources/EDMarketConnector.manifest diff --git a/EDMarketConnector.xcf b/resources/EDMarketConnector.xcf similarity index 100% rename from EDMarketConnector.xcf rename to resources/EDMarketConnector.xcf diff --git a/resources/edmc.ico b/resources/edmc.ico new file mode 100644 index 00000000..8f3f9ff4 Binary files /dev/null and b/resources/edmc.ico differ diff --git a/resources/io.edcd.EDMarketConnector.cmd.psd b/resources/io.edcd.EDMarketConnector.cmd.psd new file mode 100644 index 00000000..6bebc572 Binary files /dev/null and b/resources/io.edcd.EDMarketConnector.cmd.psd differ diff --git a/resources/ships.json b/resources/ships.json deleted file mode 100644 index 4a40ba4f..00000000 --- a/resources/ships.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "Adder": { - "hullMass": 35 - }, - "Alliance Challenger": { - "hullMass": 450 - }, - "Alliance Chieftain": { - "hullMass": 400 - }, - "Alliance Crusader": { - "hullMass": 500 - }, - "Anaconda": { - "hullMass": 400 - }, - "Asp Explorer": { - "hullMass": 280 - }, - "Asp Scout": { - "hullMass": 150 - }, - "Beluga Liner": { - "hullMass": 950 - }, - "Cobra MkIII": { - "hullMass": 180 - }, - "Cobra MkIV": { - "hullMass": 210 - }, - "Diamondback Explorer": { - "hullMass": 260 - }, - "Diamondback Scout": { - "hullMass": 170 - }, - "Dolphin": { - "hullMass": 140 - }, - "Eagle": { - "hullMass": 50 - }, - "Federal Assault Ship": { - "hullMass": 480 - }, - "Federal Corvette": { - "hullMass": 900 - }, - "Federal Dropship": { - "hullMass": 580 - }, - "Federal Gunship": { - "hullMass": 580 - }, - "Fer-de-Lance": { - "hullMass": 250 - }, - "Hauler": { - "hullMass": 14 - }, - "Imperial Clipper": { - "hullMass": 400 - }, - "Imperial Courier": { - "hullMass": 35 - }, - "Imperial Cutter": { - "hullMass": 1100 - }, - "Imperial Eagle": { - "hullMass": 50 - }, - "Keelback": { - "hullMass": 180 - }, - "Krait MkII": { - "hullMass": 320 - }, - "Krait Phantom": { - "hullMass": 270 - }, - "Mamba": { - "hullMass": 250 - }, - "Orca": { - "hullMass": 290 - }, - "Python": { - "hullMass": 350 - }, - "Sidewinder": { - "hullMass": 25 - }, - "Type-10 Defender": { - "hullMass": 1200 - }, - "Type-6 Transporter": { - "hullMass": 155 - }, - "Type-7 Transporter": { - "hullMass": 350 - }, - "Type-9 Heavy": { - "hullMass": 850 - }, - "Viper MkIII": { - "hullMass": 50 - }, - "Viper MkIV": { - "hullMass": 190 - }, - "Vulture": { - "hullMass": 230 - } -} \ No newline at end of file diff --git a/scripts/build_changelog.py b/scripts/build_changelog.py new file mode 100644 index 00000000..42157947 --- /dev/null +++ b/scripts/build_changelog.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# flake8: noqa +""" +build_changelog.py - Read the latest changelog file and format for the Forums and Reddit. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" + +import pathlib +import re +from os import chdir +import mistune + + +def get_changelog() -> tuple[str, str]: + """Pull the last full changelog details in MD.""" + try: + with open("../CHANGELOG.md", encoding="utf-8") as changelog_file: + content = changelog_file.read() + except FileNotFoundError as exc: + raise FileNotFoundError("Changelog file not found.") from exc + + changelog_list = content.split("---", maxsplit=2) + if len(changelog_list) < 3: + raise ValueError("Changelog format is incorrect.") + + changelog = changelog_list[2].split("===", maxsplit=2) + if len(changelog) < 2: + raise ValueError("Changelog format is incorrect.") + + changelog[0] = changelog[0].strip() + changelog[1] = "\n".join(changelog[1].strip().split("\n")[:-2]) + version = changelog[0] + version = version.split(" ")[1] + final_changelog = changelog[1].strip() + + return final_changelog, version + + +def build_html(md_changelog: str, version: str) -> str: + """Convert markdown changelog to HTML.""" + html_out = f"

Release {version}

\n" + html_out += mistune.html(md_changelog) # type: ignore + html_out = re.sub(r"h1", "h2", html_out) + "\n
" + + with open("script_output/html_changelog.txt", "w", encoding="utf-8") as html_file: + html_file.write(html_out) + + return html_out + + +def format_fdev(md_log: str) -> str: + """Format changelog for FDEV forums.""" + md_log = re.sub(r"

|

", "", md_log) + md_log = re.sub(r"", "\n[HEADING=3]", md_log) + md_log = re.sub(r"", "[/HEADING]", md_log) + md_log = re.sub(r"
    ", "[LIST]", md_log) + md_log = re.sub(r"
  • ", "[*]", md_log) + md_log = re.sub(r"
  • ", "", md_log) + md_log = re.sub(r"", "[ICODE]", md_log) + md_log = re.sub(r"", "[/ICODE]", md_log) + md_log = re.sub(r"
\n", "[/LIST]", md_log) + md_log = re.sub(r"
", "", md_log) + md_log = re.sub(r"Changes and Enhancements", "What's Changed", md_log) + return md_log + + +def build_fdev( + vt_signed: str, vt_unsigned: str, version: str, gh_link: str, html: str +) -> None: + """Build changelog for FDEV forums.""" + fdev_out = ( + f"[HEADING=2][URL='{gh_link}'][SIZE=7]Release {version}[/SIZE][/URL][/HEADING]\n" + f"[URL='{vt_signed}']Pre-emptive upload to VirusTotal[/URL]. " + f"([URL='{vt_unsigned}']Unsigned Installer[/URL])\n\n" + ) + + if version.startswith(("Pre-Release", "Beta")): + fdev_out += ( + f'This is a release candidate for {version}. It has been pushed to the "Beta" track for updates!\n\n' + 'For more information on the "Beta" update track, please read ' + "[URL='https://github.com/EDCD/EDMarketConnector/wiki/Participating-in-Open-Betas-of-EDMC']" + "This Wiki Article[/URL]. Questions and comments are welcome!\n\n" + ) + + md_log = html.split("\n", maxsplit=1)[1] + md_log = format_fdev(md_log) + fdev_out += md_log + + with open("script_output/fdev_changelog.txt", "w", encoding="utf-8") as fdev_file: + fdev_file.write(fdev_out) + + +def build_reddit( + md_changelog: str, vt_signed: str, vt_unsigned: str, version: str, gh_link: str +) -> None: + """Build changelog for Reddit.""" + reddit_start = """# What Is Elite Dangerous Market Connector? + +Elite Dangerous Market Connector ("EDMC") is a third-party application for use with Frontier Developments' game "Elite Dangerous". Its purpose is to facilitate supplying certain game data to, and in some cases retrieving it from, a number of websites and other tools. + +To achieve this it utilizes the Journal Files written by the game when played on a PC. It also makes use of Frontier's Companion API ("Frontier's CAPI"), accessible once you've authorized this application. + +EDMC has a plugin system that many other developers have made use of to extend its functionality. + +Find out more on the [EDMC Wiki](https://github.com/EDCD/EDMarketConnector/wiki). + +~~----------------------------------------------------~~ + +You can also view the Elite: Dangerous Forum thread [HERE](https://forums.frontier.co.uk/threads/elite-dangerous-market-connector-edmc.618708/). + +~~----------------------------------------------------~~ + +**As has become routine now, various anti-virus software are reporting a false positive on our installer and/or files it contains. We've pre-emptively uploaded the installer to** [VirusTotal](""" + reddit_mid_1 = """) **if you want to check what it's saying. Please see our** [Troubleshooting/AV-false-positives FAQ](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#installer-and-or-executables-flagged-as-malicious-viruses) **for further information.** + +[Unsigned Installer](""" + + reddit_mid_2 = """) if you don't want to use the code-signed option. + +~~----------------------------------------------------~~ +""" + updated = f"# [Release {version}]({gh_link})\n\n" + md_changelog = re.sub(r"===\n", "", md_changelog) + md_changelog = re.sub(f"Release {version}", updated, md_changelog) + reddit_end = f""" + +**Linux** + +If you're running on Linux, try the [Flatpak](https://flathub.org/apps/io.edcd.EDMarketConnector) build of EDMC! (Update to {version} coming soon.)""" + + reddit_out = ( + reddit_start + + vt_signed + + reddit_mid_1 + + vt_unsigned + + reddit_mid_2 + + updated + + md_changelog + + reddit_end + ) + + with open( + "script_output/reddit_changelog.txt", "w", encoding="utf-8" + ) as reddit_file: + reddit_file.write(reddit_out) + + +def main() -> None: + """Run the Changelog Generator""" + md_changelog, version = get_changelog() + print(f"Detected version {version} in the changelog. Continuing...") + gh_link = input(f"Please enter the GitHub link for {version}: ") + vt_signed = input("Please enter the VirusTotal URL for the Signed Installer: ") + vt_unsigned = input("Please enter the VirusTotal URL for the Unsigned Installer: ") + build_reddit(md_changelog, vt_signed, vt_unsigned, version, gh_link) + html = build_html(md_changelog, version) + build_fdev(vt_signed, vt_unsigned, version, gh_link, html) + + +if __name__ == "__main__": + chdir(pathlib.Path(__file__).parent) + main() diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 1d1da826..8187f262 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.""" + from __future__ import annotations import argparse @@ -17,20 +18,20 @@ def get_func_name(thing: ast.AST) -> str: if isinstance(thing, ast.Attribute): return get_func_name(thing.value) - 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 if isinstance(arg, ast.Name): - return f'VARIABLE! CHECK CODE! {arg.id}' - return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}' + return f"VARIABLE! CHECK CODE! {arg.id}" + return f"Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}" def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: @@ -38,8 +39,15 @@ def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: out = [] for n in ast.iter_child_nodes(statement): out.extend(find_calls_in_stmt(n)) - if isinstance(statement, ast.Call) and get_func_name(statement.func) == '_': - out.append(statement) + if isinstance(statement, ast.Call) and get_func_name(statement.func) in ( + "tr", + "translations", + ): + if ( + ast.unparse(statement).find(".tl") != -1 + or ast.unparse(statement).find("translate") != -1 + ): + out.append(statement) return out @@ -52,11 +60,13 @@ COMMENT_OWN_LINE_RE is for a comment on its own line. The difference is necessary in order to tell if a 'above' LANG comment is for its own line (SAME_LINE), or meant to be for this following line (OWN_LINE). """ -COMMENT_SAME_LINE_RE = re.compile(r'^.*?(#.*)$') -COMMENT_OWN_LINE_RE = re.compile(r'^\s*?(#.*)$') +COMMENT_SAME_LINE_RE = re.compile(r"^.*?(#.*)$") +COMMENT_OWN_LINE_RE = re.compile(r"^\s*?(#.*)$") -def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> str | None: # noqa: CCR001 +def extract_comments( # noqa: CCR001 + call: ast.Call, lines: list[str], file: pathlib.Path +) -> str | None: """ Extract comments from source code based on the given call. @@ -82,23 +92,23 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> st match = COMMENT_OWN_LINE_RE.match(above_line) if match: above_comment = match.group(1).strip() - if not above_comment.startswith('# LANG:'): - bad_comment = f'Unknown comment for {file}:{call.lineno} {above_line}' + if not above_comment.startswith("# LANG:"): + bad_comment = f"Unknown comment for {file}:{call.lineno} {above_line}" above_comment = None else: - above_comment = above_comment.replace('# LANG:', '').strip() + above_comment = above_comment.replace("# LANG:", "").strip() if current_line is not None: match = COMMENT_SAME_LINE_RE.match(current_line) if match: current_comment = match.group(1).strip() - if not current_comment.startswith('# LANG:'): - bad_comment = f'Unknown comment for {file}:{call.lineno} {current_line}' + if not current_comment.startswith("# LANG:"): + bad_comment = f"Unknown comment for {file}:{call.lineno} {current_line}" current_comment = None else: - current_comment = current_comment.replace('# LANG:', '').strip() + current_comment = current_comment.replace("# LANG:", "").strip() if current_comment is not None: out = current_comment @@ -108,13 +118,13 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> st print(bad_comment, file=sys.stderr) if out is None: - print(f'No comment for {file}:{call.lineno} {current_line}', file=sys.stderr) + print(f"No comment for {file}:{call.lineno} {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') + data = path.read_text(encoding="utf-8") lines = data.splitlines() parsed = ast.parse(data) out: list[ast.Call] = [] @@ -124,13 +134,15 @@ 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 -def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] | None = None) -> dict[pathlib.Path, list[ast.Call]]: +def scan_directory( + path: pathlib.Path, skip: list[pathlib.Path] | None = None +) -> dict[pathlib.Path, list[ast.Call]]: """ Scan a directory for expected callsites. @@ -144,7 +156,7 @@ def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] | None = None) - if any(same_path.name == thing.name for same_path in skip): continue - if thing.is_file() and thing.suffix == '.py': + if thing.is_file() and thing.suffix == ".py": out[thing] = scan_file(thing) elif thing.is_dir(): out.update(scan_directory(thing, skip)) @@ -162,10 +174,10 @@ def parse_template(path) -> set[str]: """ lang_re = re.compile(r'\s*"([^"]+)"\s*=\s*"([^"]+)"\s*;\s*$') out = set() - with open(path, encoding='utf-8') as file: + with open(path, encoding="utf-8") as file: for line in file: match = lang_re.match(line.strip()) - if match and match.group(1) != '!Language': + if match and match.group(1) != "!Language": out.add(match.group(1)) return out @@ -182,14 +194,16 @@ class FileLocation: line_end_col: int | None @staticmethod - def from_call(path: pathlib.Path, c: ast.Call) -> 'FileLocation': + 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) + return FileLocation( + path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset + ) @dataclasses.dataclass @@ -237,95 +251,121 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: 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')])) + 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) +""" + print(f"Done Deduping entries {len(entries)=} {len(deduped)=}", file=sys.stderr) for entry in deduped: assert len(entry.comments) == len(entry.locations) comment_set = set() for comment, loc in zip(entry.comments, entry.locations): if comment: - comment_set.add(f'{loc.path.name}: {comment};') + comment_set.add(f"{loc.path.name}: {comment};") - files = 'In files: ' + entry.files() - comment = ' '.join(comment_set).strip() + files = "In files: " + entry.files() + comment = " ".join(comment_set).strip() - header = f'{comment} {files}'.strip() + header = f"{comment} {files}".strip() string = f'"{entry.string}"' - out += f'/* {header} */\n' - out += f'{string} = {string};\n\n' + out += f"/* {header} */\n" + out += f"{string} = {string};\n\n" return out -if __name__ == '__main__': +def main(): # noqa: CCR001 + """Run the Translation Checker.""" 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', '.venv', '.git']) + parser.add_argument("--directory", help="Directory to search from", default=".") + parser.add_argument( + "--ignore", + action="append", + help="Directories to ignore", + default=["venv", ".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') + 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: + output = [] + + if args.compare_lang: 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}') + output.append(f"NEW! {file}:{c.lineno}: {arg!r}") for old in set(template) ^ seen: - print(f'No longer used: {old}') + output.append(f"No longer used: {old!r}") 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 + "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)) + output.append(json.dumps(to_print_data, indent=2)) elif args.lang: - if args.lang == '-': - print(generate_lang_template(res)) - + lang_template = generate_lang_template(res) + if args.lang == "-": + output.append(lang_template) else: - with open(args.lang, mode='w+', newline='\n') as langfile: - langfile.writelines(generate_lang_template(res)) + with open(args.lang, mode="w+", newline="\n", encoding="UTF-8") as langfile: + langfile.writelines(lang_template) else: for path, calls in res.items(): - if len(calls) == 0: + if not calls: continue - - print(path) + output.append(str(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) + output.append( + f" {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t{ast.unparse(c)}" ) + output.append("") - print() + # Print all collected output at the end + if output: + print("\n".join(output)) + sys.exit(1) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + sys.exit() diff --git a/scripts/pip_rev_deps.py b/scripts/pip_rev_deps.py deleted file mode 100644 index 1956f5d0..00000000 --- a/scripts/pip_rev_deps.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -"""Search for dependencies given a package.""" -import sys - -import pkg_resources - - -def find_reverse_deps(package_name: str) -> list[str]: - """ - Find the packages that depend on the named one. - - :param package_name: Target package. - :return: List of packages that depend on this one. - """ - return [ - pkg.project_name for pkg in pkg_resources.WorkingSet() - if package_name in {req.project_name for req in pkg.requires()} - ] - - -if __name__ == '__main__': - if len(sys.argv) != 2: - print("Usage: python reverse_deps.py ") - sys.exit(1) - - package_name = sys.argv[1] - reverse_deps = find_reverse_deps(package_name) - - if reverse_deps: - print(f"Reverse dependencies of '{package_name}':") - for dep in reverse_deps: - print(dep) - else: - print(f"No reverse dependencies found for '{package_name}'.") diff --git a/ships.json b/ships.json new file mode 100644 index 00000000..363cd561 --- /dev/null +++ b/ships.json @@ -0,0 +1,166 @@ +{ + "Adder": { + "hullMass": 35, + "reserveFuelCapacity": 0.36 + }, + "Alliance Challenger": { + "hullMass": 450, + "reserveFuelCapacity": 0.77 + }, + "Alliance Chieftain": { + "hullMass": 400, + "reserveFuelCapacity": 0.77 + }, + "Alliance Crusader": { + "hullMass": 500, + "reserveFuelCapacity": 0.77 + }, + "Anaconda": { + "hullMass": 400, + "reserveFuelCapacity": 1.07 + }, + "Asp Explorer": { + "hullMass": 280, + "reserveFuelCapacity": 0.63 + }, + "Asp Scout": { + "hullMass": 150, + "reserveFuelCapacity": 0.47 + }, + "Beluga Liner": { + "hullMass": 950, + "reserveFuelCapacity": 0.81 + }, + "Cobra MkIII": { + "hullMass": 180, + "reserveFuelCapacity": 0.49 + }, + "Cobra MkIV": { + "hullMass": 210, + "reserveFuelCapacity": 0.51 + }, + "Diamondback Explorer": { + "hullMass": 260, + "reserveFuelCapacity": 0.52 + }, + "Diamondback Scout": { + "hullMass": 170, + "reserveFuelCapacity": 0.49 + }, + "Dolphin": { + "hullMass": 140, + "reserveFuelCapacity": 0.5 + }, + "Eagle": { + "hullMass": 50, + "reserveFuelCapacity": 0.34 + }, + "Federal Assault Ship": { + "hullMass": 480, + "reserveFuelCapacity": 0.72 + }, + "Federal Corvette": { + "hullMass": 900, + "reserveFuelCapacity": 1.13 + }, + "Federal Dropship": { + "hullMass": 580, + "reserveFuelCapacity": 0.83 + }, + "Federal Gunship": { + "hullMass": 580, + "reserveFuelCapacity": 0.82 + }, + "Fer-de-Lance": { + "hullMass": 250, + "reserveFuelCapacity": 0.67 + }, + "Hauler": { + "hullMass": 14, + "reserveFuelCapacity": 0.25 + }, + "Imperial Clipper": { + "hullMass": 400, + "reserveFuelCapacity": 0.74 + }, + "Imperial Courier": { + "hullMass": 35, + "reserveFuelCapacity": 0.41 + }, + "Imperial Cutter": { + "hullMass": 1100, + "reserveFuelCapacity": 1.16 + }, + "Imperial Eagle": { + "hullMass": 50, + "reserveFuelCapacity": 0.37 + }, + "Keelback": { + "hullMass": 180, + "reserveFuelCapacity": 0.39 + }, + "Krait MkII": { + "hullMass": 320, + "reserveFuelCapacity": 0.63 + }, + "Krait Phantom": { + "hullMass": 270, + "reserveFuelCapacity": 0.63 + }, + "Mamba": { + "hullMass": 250, + "reserveFuelCapacity": 0.5 + }, + "Mandalay": { + "hullMass": 230, + "reserveFuelCapacity": 0.52 + }, + "Orca": { + "hullMass": 290, + "reserveFuelCapacity": 0.79 + }, + "Python": { + "hullMass": 350, + "reserveFuelCapacity": 0.83 + }, + "Python Mk II": { + "hullMass": 450, + "reserveFuelCapacity": 0.83 + }, + "Sidewinder": { + "hullMass": 25, + "reserveFuelCapacity": 0.3 + }, + "Type-10 Defender": { + "hullMass": 1200, + "reserveFuelCapacity": 0.77 + }, + "Type-6 Transporter": { + "hullMass": 155, + "reserveFuelCapacity": 0.39 + }, + "Type-7 Transporter": { + "hullMass": 350, + "reserveFuelCapacity": 0.52 + }, + "Type-8 Transporter": { + "hullMass": 400, + "reserveFuelCapacity": 0.52 + }, + "Type-9 Heavy": { + "hullMass": 850, + "reserveFuelCapacity": 0.77 + }, + "Viper MkIII": { + "hullMass": 50, + "reserveFuelCapacity": 0.41 + }, + "Viper MkIV": { + "hullMass": 190, + "reserveFuelCapacity": 0.46 + }, + "Vulture": { + "hullMass": 230, + "reserveFuelCapacity": 0.57 + } +} \ No newline at end of file diff --git a/ships.p b/ships.p deleted file mode 100644 index d289497c..00000000 Binary files a/ships.p and /dev/null differ diff --git a/stats.py b/stats.py index 041e47f3..b0ba34b3 100644 --- a/stats.py +++ b/stats.py @@ -5,38 +5,35 @@ Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. """ +from __future__ import annotations + import csv import json import sys import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any, AnyStr, Callable, NamedTuple, Sequence, cast +from typing import Any, AnyStr, Callable, NamedTuple, Sequence, cast import companion import EDMCLogging import myNotebook as nb # noqa: N813 from edmc_data import ship_name_map from hotkey import hotkeymgr -from l10n import Locale +from l10n import Locale, translations as tr from monitor import monitor logger = EDMCLogging.get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: ... - if sys.platform == 'win32': import ctypes - from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT + from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL + import win32gui try: CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition CalculatePopupWindowPosition.argtypes = [ ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] + CalculatePopupWindowPosition.restype = BOOL except Exception: # Not supported under Wine 4.0 CalculatePopupWindowPosition = None # type: ignore @@ -58,32 +55,32 @@ def status(data: dict[str, Any]) -> list[list[str]]: """ # StatsResults assumes these three things are first res = [ - [_('Cmdr'), data['commander']['name']], # LANG: Cmdr stats - [_('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats - [_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats + [tr.tl('Cmdr'), data['commander']['name']], # LANG: Cmdr stats + [tr.tl('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats + [tr.tl('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats ] _ELITE_RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime - _('Elite'), # LANG: Top rank - _('Elite I'), # LANG: Top rank +1 - _('Elite II'), # LANG: Top rank +2 - _('Elite III'), # LANG: Top rank +3 - _('Elite IV'), # LANG: Top rank +4 - _('Elite V'), # LANG: Top rank +5 + tr.tl('Elite'), # LANG: Top rank + tr.tl('Elite I'), # LANG: Top rank +1 + tr.tl('Elite II'), # LANG: Top rank +2 + tr.tl('Elite III'), # LANG: Top rank +3 + tr.tl('Elite IV'), # LANG: Top rank +4 + tr.tl('Elite V'), # LANG: Top rank +5 ] RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime # in output order # Names we show people, vs internal names - (_('Combat'), 'combat'), # LANG: Ranking - (_('Trade'), 'trade'), # LANG: Ranking - (_('Explorer'), 'explore'), # LANG: Ranking - (_('Mercenary'), 'soldier'), # LANG: Ranking - (_('Exobiologist'), 'exobiologist'), # LANG: Ranking - (_('CQC'), 'cqc'), # LANG: Ranking - (_('Federation'), 'federation'), # LANG: Ranking - (_('Empire'), 'empire'), # LANG: Ranking - (_('Powerplay'), 'power'), # LANG: Ranking + (tr.tl('Combat'), 'combat'), # LANG: Ranking + (tr.tl('Trade'), 'trade'), # LANG: Ranking + (tr.tl('Explorer'), 'explore'), # LANG: Ranking + (tr.tl('Mercenary'), 'soldier'), # LANG: Ranking + (tr.tl('Exobiologist'), 'exobiologist'), # LANG: Ranking + (tr.tl('CQC'), 'cqc'), # LANG: Ranking + (tr.tl('Federation'), 'federation'), # LANG: Ranking + (tr.tl('Empire'), 'empire'), # LANG: Ranking + (tr.tl('Powerplay'), 'power'), # LANG: Ranking # ??? , 'crime'), # LANG: Ranking # ??? , 'service'), # LANG: Ranking ] @@ -92,113 +89,113 @@ def status(data: dict[str, Any]) -> list[list[str]]: # These names are the fdev side name (but lower()ed) # http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks 'combat': [ - _('Harmless'), # LANG: Combat rank - _('Mostly Harmless'), # LANG: Combat rank - _('Novice'), # LANG: Combat rank - _('Competent'), # LANG: Combat rank - _('Expert'), # LANG: Combat rank - _('Master'), # LANG: Combat rank - _('Dangerous'), # LANG: Combat rank - _('Deadly'), # LANG: Combat rank + tr.tl('Harmless'), # LANG: Combat rank + tr.tl('Mostly Harmless'), # LANG: Combat rank + tr.tl('Novice'), # LANG: Combat rank + tr.tl('Competent'), # LANG: Combat rank + tr.tl('Expert'), # LANG: Combat rank + tr.tl('Master'), # LANG: Combat rank + tr.tl('Dangerous'), # LANG: Combat rank + tr.tl('Deadly'), # LANG: Combat rank ] + _ELITE_RANKS, 'trade': [ - _('Penniless'), # LANG: Trade rank - _('Mostly Penniless'), # LANG: Trade rank - _('Peddler'), # LANG: Trade rank - _('Dealer'), # LANG: Trade rank - _('Merchant'), # LANG: Trade rank - _('Broker'), # LANG: Trade rank - _('Entrepreneur'), # LANG: Trade rank - _('Tycoon'), # LANG: Trade rank + tr.tl('Penniless'), # LANG: Trade rank + tr.tl('Mostly Penniless'), # LANG: Trade rank + tr.tl('Peddler'), # LANG: Trade rank + tr.tl('Dealer'), # LANG: Trade rank + tr.tl('Merchant'), # LANG: Trade rank + tr.tl('Broker'), # LANG: Trade rank + tr.tl('Entrepreneur'), # LANG: Trade rank + tr.tl('Tycoon'), # LANG: Trade rank ] + _ELITE_RANKS, 'explore': [ - _('Aimless'), # LANG: Explorer rank - _('Mostly Aimless'), # LANG: Explorer rank - _('Scout'), # LANG: Explorer rank - _('Surveyor'), # LANG: Explorer rank - _('Trailblazer'), # LANG: Explorer rank - _('Pathfinder'), # LANG: Explorer rank - _('Ranger'), # LANG: Explorer rank - _('Pioneer'), # LANG: Explorer rank + tr.tl('Aimless'), # LANG: Explorer rank + tr.tl('Mostly Aimless'), # LANG: Explorer rank + tr.tl('Scout'), # LANG: Explorer rank + tr.tl('Surveyor'), # LANG: Explorer rank + tr.tl('Trailblazer'), # LANG: Explorer rank + tr.tl('Pathfinder'), # LANG: Explorer rank + tr.tl('Ranger'), # LANG: Explorer rank + tr.tl('Pioneer'), # LANG: Explorer rank ] + _ELITE_RANKS, 'soldier': [ - _('Defenceless'), # LANG: Mercenary rank - _('Mostly Defenceless'), # LANG: Mercenary rank - _('Rookie'), # LANG: Mercenary rank - _('Soldier'), # LANG: Mercenary rank - _('Gunslinger'), # LANG: Mercenary rank - _('Warrior'), # LANG: Mercenary rank - _('Gunslinger'), # LANG: Mercenary rank - _('Deadeye'), # LANG: Mercenary rank + tr.tl('Defenceless'), # LANG: Mercenary rank + tr.tl('Mostly Defenceless'), # LANG: Mercenary rank + tr.tl('Rookie'), # LANG: Mercenary rank + tr.tl('Soldier'), # LANG: Mercenary rank + tr.tl('Gunslinger'), # LANG: Mercenary rank + tr.tl('Warrior'), # LANG: Mercenary rank + tr.tl('Gunslinger'), # LANG: Mercenary rank + tr.tl('Deadeye'), # LANG: Mercenary rank ] + _ELITE_RANKS, 'exobiologist': [ - _('Directionless'), # LANG: Exobiologist rank - _('Mostly Directionless'), # LANG: Exobiologist rank - _('Compiler'), # LANG: Exobiologist rank - _('Collector'), # LANG: Exobiologist rank - _('Cataloguer'), # LANG: Exobiologist rank - _('Taxonomist'), # LANG: Exobiologist rank - _('Ecologist'), # LANG: Exobiologist rank - _('Geneticist'), # LANG: Exobiologist rank + tr.tl('Directionless'), # LANG: Exobiologist rank + tr.tl('Mostly Directionless'), # LANG: Exobiologist rank + tr.tl('Compiler'), # LANG: Exobiologist rank + tr.tl('Collector'), # LANG: Exobiologist rank + tr.tl('Cataloguer'), # LANG: Exobiologist rank + tr.tl('Taxonomist'), # LANG: Exobiologist rank + tr.tl('Ecologist'), # LANG: Exobiologist rank + tr.tl('Geneticist'), # LANG: Exobiologist rank ] + _ELITE_RANKS, 'cqc': [ - _('Helpless'), # LANG: CQC rank - _('Mostly Helpless'), # LANG: CQC rank - _('Amateur'), # LANG: CQC rank - _('Semi Professional'), # LANG: CQC rank - _('Professional'), # LANG: CQC rank - _('Champion'), # LANG: CQC rank - _('Hero'), # LANG: CQC rank - _('Gladiator'), # LANG: CQC rank + tr.tl('Helpless'), # LANG: CQC rank + tr.tl('Mostly Helpless'), # LANG: CQC rank + tr.tl('Amateur'), # LANG: CQC rank + tr.tl('Semi Professional'), # LANG: CQC rank + tr.tl('Professional'), # LANG: CQC rank + tr.tl('Champion'), # LANG: CQC rank + tr.tl('Hero'), # LANG: CQC rank + tr.tl('Gladiator'), # LANG: CQC rank ] + _ELITE_RANKS, # http://elite-dangerous.wikia.com/wiki/Federation#Ranks 'federation': [ - _('None'), # LANG: No rank - _('Recruit'), # LANG: Federation rank - _('Cadet'), # LANG: Federation rank - _('Midshipman'), # LANG: Federation rank - _('Petty Officer'), # LANG: Federation rank - _('Chief Petty Officer'), # LANG: Federation rank - _('Warrant Officer'), # LANG: Federation rank - _('Ensign'), # LANG: Federation rank - _('Lieutenant'), # LANG: Federation rank - _('Lieutenant Commander'), # LANG: Federation rank - _('Post Commander'), # LANG: Federation rank - _('Post Captain'), # LANG: Federation rank - _('Rear Admiral'), # LANG: Federation rank - _('Vice Admiral'), # LANG: Federation rank - _('Admiral') # LANG: Federation rank + tr.tl('None'), # LANG: No rank + tr.tl('Recruit'), # LANG: Federation rank + tr.tl('Cadet'), # LANG: Federation rank + tr.tl('Midshipman'), # LANG: Federation rank + tr.tl('Petty Officer'), # LANG: Federation rank + tr.tl('Chief Petty Officer'), # LANG: Federation rank + tr.tl('Warrant Officer'), # LANG: Federation rank + tr.tl('Ensign'), # LANG: Federation rank + tr.tl('Lieutenant'), # LANG: Federation rank + tr.tl('Lieutenant Commander'), # LANG: Federation rank + tr.tl('Post Commander'), # LANG: Federation rank + tr.tl('Post Captain'), # LANG: Federation rank + tr.tl('Rear Admiral'), # LANG: Federation rank + tr.tl('Vice Admiral'), # LANG: Federation rank + tr.tl('Admiral') # LANG: Federation rank ], # http://elite-dangerous.wikia.com/wiki/Empire#Ranks 'empire': [ - _('None'), # LANG: No rank - _('Outsider'), # LANG: Empire rank - _('Serf'), # LANG: Empire rank - _('Master'), # LANG: Empire rank - _('Squire'), # LANG: Empire rank - _('Knight'), # LANG: Empire rank - _('Lord'), # LANG: Empire rank - _('Baron'), # LANG: Empire rank - _('Viscount'), # LANG: Empire rank - _('Count'), # LANG: Empire rank - _('Earl'), # LANG: Empire rank - _('Marquis'), # LANG: Empire rank - _('Duke'), # LANG: Empire rank - _('Prince'), # LANG: Empire rank - _('King') # LANG: Empire rank + tr.tl('None'), # LANG: No rank + tr.tl('Outsider'), # LANG: Empire rank + tr.tl('Serf'), # LANG: Empire rank + tr.tl('Master'), # LANG: Empire rank + tr.tl('Squire'), # LANG: Empire rank + tr.tl('Knight'), # LANG: Empire rank + tr.tl('Lord'), # LANG: Empire rank + tr.tl('Baron'), # LANG: Empire rank + tr.tl('Viscount'), # LANG: Empire rank + tr.tl('Count'), # LANG: Empire rank + tr.tl('Earl'), # LANG: Empire rank + tr.tl('Marquis'), # LANG: Empire rank + tr.tl('Duke'), # LANG: Empire rank + tr.tl('Prince'), # LANG: Empire rank + tr.tl('King') # LANG: Empire rank ], # http://elite-dangerous.wikia.com/wiki/Ratings 'power': [ - _('None'), # LANG: No rank - _('Rating 1'), # LANG: Power rank - _('Rating 2'), # LANG: Power rank - _('Rating 3'), # LANG: Power rank - _('Rating 4'), # LANG: Power rank - _('Rating 5') # LANG: Power rank + tr.tl('None'), # LANG: No rank + tr.tl('Rating 1'), # LANG: Power rank + tr.tl('Rating 2'), # LANG: Power rank + tr.tl('Rating 3'), # LANG: Power rank + tr.tl('Rating 4'), # LANG: Power rank + tr.tl('Rating 5') # LANG: Power rank ], } @@ -210,7 +207,7 @@ def status(data: dict[str, Any]) -> list[list[str]]: res.append([title, names[rank] if rank < len(names) else f'Rank {rank}']) else: - res.append([title, _('None')]) # LANG: No rank + res.append([title, tr.tl('None')]) # LANG: No rank return res @@ -244,7 +241,7 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]: """ Return a list of 5 tuples of ship information. - :param data: [description] + :param companion_data: [description] :return: A 5 tuple of strings containing: Ship ID, Ship Type Name (internal), Ship Name, System, Station, and Value """ ships: list[dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships'))) @@ -254,16 +251,15 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]: ships.insert(0, ships.pop(current)) # Put current ship first if not companion_data['commander'].get('docked'): - out: list[ShipRet] = [] # Set current system, not last docked - out.append(ShipRet( + out: list[ShipRet] = [ShipRet( id=str(ships[0]['id']), type=ship_name_map.get(ships[0]['name'].lower(), ships[0]['name']), name=str(ships[0].get('shipName', '')), system=companion_data['lastSystem']['name'], station='', value=str(ships[0]['value']['total']) - )) + )] out.extend( ShipRet( id=str(ship['id']), @@ -303,7 +299,7 @@ def export_ships(companion_data: dict[str, Any], filename: AnyStr) -> None: h.writerow(list(thing)) -class StatsDialog(): +class StatsDialog: """Status dialog containing all of the current cmdr's stats.""" def __init__(self, parent: tk.Tk, status: tk.Label) -> None: @@ -316,7 +312,7 @@ class StatsDialog(): if not monitor.cmdr: hotkeymgr.play_bad() # LANG: Current commander unknown when trying to use 'File' > 'Status' - self.status['text'] = _("Status: Don't yet know your Commander name") + self.status['text'] = tr.tl("Status: Don't yet know your Commander name") return # TODO: This needs to use cached data @@ -324,7 +320,7 @@ class StatsDialog(): logger.info('No cached data, aborting...') hotkeymgr.play_bad() # LANG: No Frontier CAPI data yet when trying to use 'File' > 'Status' - self.status['text'] = _("Status: No CAPI data yet") + self.status['text'] = tr.tl("Status: No CAPI data yet") return capi_data = json.loads( @@ -334,7 +330,7 @@ class StatsDialog(): if not capi_data.get('commander') or not capi_data['commander'].get('name', '').strip(): # Shouldn't happen # LANG: Unknown commander - self.status['text'] = _("Who are you?!") + self.status['text'] = tr.tl("Who are you?!") elif ( not capi_data.get('lastSystem') @@ -342,7 +338,7 @@ class StatsDialog(): ): # Shouldn't happen # LANG: Unknown location - self.status['text'] = _("Where are you?!") + self.status['text'] = tr.tl("Where are you?!") elif ( not capi_data.get('ship') or not capi_data['ship'].get('modules') @@ -350,7 +346,7 @@ class StatsDialog(): ): # Shouldn't happen # LANG: Unknown ship - self.status['text'] = _("What are you flying?!") + self.status['text'] = tr.tl("What are you flying?!") else: self.status['text'] = '' @@ -372,7 +368,7 @@ class StatsResults(tk.Toplevel): self.transient(parent) # position over parent - if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 + if parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.geometry(f"+{parent.winfo_rootx()}+{parent.winfo_rooty()}") # remove decoration @@ -380,10 +376,6 @@ class StatsResults(tk.Toplevel): if sys.platform == 'win32': self.attributes('-toolwindow', tk.TRUE) - elif sys.platform == 'darwin': - # http://wiki.tcl.tk/13428 - parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') - frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) @@ -403,14 +395,14 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, thing, with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Status')) # LANG: Status dialog title + notebook.add(page, text=tr.tl('Status')) # LANG: Status dialog title page = self.addpage(notebook, [ - _('Ship'), # LANG: Status dialog subtitle + tr.tl('Ship'), # LANG: Status dialog subtitle '', - _('System'), # LANG: Main window - _('Station'), # LANG: Status dialog subtitle - _('Value'), # LANG: Status dialog subtitle - CR value of ship + tr.tl('System'), # LANG: Main window + tr.tl('Station'), # LANG: Status dialog subtitle + tr.tl('Value'), # LANG: Status dialog subtitle - CR value of ship ]) shiplist = ships(data) @@ -419,14 +411,7 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, list(ship_data[1:-1]) + [self.credits(int(ship_data[-1]))], with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Ships')) # LANG: Status dialog title - - if sys.platform != 'darwin': - buttonframe = ttk.Frame(frame) - buttonframe.grid(padx=10, pady=(0, 10), sticky=tk.NSEW) # type: ignore # the tuple is supported - buttonframe.columnconfigure(0, weight=1) - ttk.Label(buttonframe).grid(row=0, column=0) # spacer - ttk.Button(buttonframe, text='OK', command=self.destroy).grid(row=0, column=1, sticky=tk.E) + notebook.add(page, text=tr.tl('Ships')) # LANG: Status dialog title # wait for window to appear on screen before calling grab_set self.wait_visibility() @@ -435,7 +420,7 @@ class StatsResults(tk.Toplevel): # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), # - is evidently supported on the C side diff --git a/td.py b/td.py index 00dd1736..484e8d29 100644 --- a/td.py +++ b/td.py @@ -1,7 +1,6 @@ """Export data for Trade Dangerous.""" import pathlib -import sys import time from collections import defaultdict from operator import itemgetter @@ -32,7 +31,7 @@ def export(data: CAPIData) -> None: with open(data_path / data_filename, 'wb') as h: # Format described here: https://github.com/eyeonus/Trade-Dangerous/wiki/Price-Data h.write('#! trade.py import -\n'.encode('utf-8')) - this_platform = sys.platform == 'darwin' and "Mac OS" or system() + this_platform = system() cmdr_name = data['commander']['name'].strip() h.write( f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8') diff --git a/tests/config/_old_config.py b/tests/config/_old_config.py index 22f0b18f..2b5244b6 100644 --- a/tests/config/_old_config.py +++ b/tests/config/_old_config.py @@ -5,21 +5,15 @@ import numbers import sys import warnings from configparser import NoOptionError -from os import getenv, makedirs, mkdir, pardir -from os.path import dirname, expanduser, isdir, join, normpath +from os import getenv, makedirs, mkdir +from os.path import dirname, expanduser, isdir, join from typing import TYPE_CHECKING from config import applongname, appname, update_interval from EDMCLogging import get_main_logger logger = get_main_logger() -if sys.platform == 'darwin': - from Foundation import ( # type: ignore - NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, - NSUserDefaults, NSUserDomainMask - ) - -elif sys.platform == 'win32': +if sys.platform == 'win32': import ctypes import uuid from ctypes.wintypes import DWORD, HANDLE, HKEY, LONG, LPCVOID, LPCWSTR @@ -115,91 +109,7 @@ class OldConfig: OUT_EDDN_DELAY = 4096 OUT_STATION_ANY = OUT_EDDN_SEND_STATION_DATA | OUT_MKT_TD | OUT_MKT_CSV - if sys.platform == 'darwin': # noqa: C901 # It's gating *all* the functions - - def __init__(self): - self.app_dir = join( - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], appname - ) - if not isdir(self.app_dir): - mkdir(self.app_dir) - - self.plugin_dir = join(self.app_dir, 'plugins') - if not isdir(self.plugin_dir): - mkdir(self.plugin_dir) - - if getattr(sys, 'frozen', False): - self.internal_plugin_dir = normpath(join(dirname(sys.executable), pardir, 'Library', 'plugins')) - self.respath = normpath(join(dirname(sys.executable), pardir, 'Resources')) - self.identifier = NSBundle.mainBundle().bundleIdentifier() - - else: - self.internal_plugin_dir = join(dirname(__file__), 'plugins') - self.respath = dirname(__file__) - # Don't use Python's settings if interactive - self.identifier = f'uk.org.marginal.{appname.lower()}' - NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier - - self.default_journal_dir: str | None = join( - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], - 'Frontier Developments', - 'Elite Dangerous' - ) - self.home = expanduser('~') - - self.defaults = NSUserDefaults.standardUserDefaults() - self.settings = dict(self.defaults.persistentDomainForName_(self.identifier) or {}) # make writeable - - # Check out_dir exists - if not self.get('outdir') or not isdir(str(self.get('outdir'))): - self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0]) - - def get(self, key: str, default: None | list | str = None) -> None | list | str: - """Look up a string configuration value.""" - val = self.settings.get(key) - if val is None: - return default - - if isinstance(val, str): - return str(val) - - if isinstance(val, list): - return list(val) # make writeable - - return default - - def getint(self, key: str, default: int = 0) -> int: - """Look up an integer configuration value.""" - try: - return int(self.settings.get(key, default)) # should already be int, but check by casting - - except ValueError as e: - logger.error(f"Failed to int({key=})", exc_info=e) - return default - - except Exception as e: - logger.debug('The exception type is ...', exc_info=e) - return default - - def set(self, key: str, val: int | str | list) -> None: - """Set value on the specified configuration key.""" - self.settings[key] = val - - def delete(self, key: str) -> None: - """Delete the specified configuration key.""" - self.settings.pop(key, None) - - def save(self) -> None: - """Save current configuration to disk.""" - self.defaults.setPersistentDomain_forName_(self.settings, self.identifier) - self.defaults.synchronize() - - def close(self) -> None: - """Close the configuration.""" - self.save() - self.defaults = None - - elif sys.platform == 'win32': + if sys.platform == 'win32': # noqa: C901 def __init__(self): self.app_dir = join(known_folder_path(FOLDERID_LocalAppData), appname) # type: ignore @@ -296,7 +206,7 @@ class OldConfig: None, ctypes.byref(key_size) ) - or key_type.value not in [REG_SZ, REG_MULTI_SZ] + or key_type.value not in (REG_SZ, REG_MULTI_SZ) ): return default diff --git a/theme.py b/theme.py index bfb76c55..5eed9fe1 100644 --- a/theme.py +++ b/theme.py @@ -13,20 +13,16 @@ from __future__ import annotations import os import sys import tkinter as tk -from os.path import join from tkinter import font as tk_font from tkinter import ttk -from typing import TYPE_CHECKING, Callable - +from typing import Callable +from l10n import translations as tr from config import config from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() -if TYPE_CHECKING: - def _(x: str) -> str: ... - if __debug__: from traceback import print_exc @@ -37,11 +33,13 @@ if sys.platform == "linux": if sys.platform == 'win32': import ctypes from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR + import win32gui AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore FR_PRIVATE = 0x10 FR_NOT_ENUM = 0x20 - AddFontResourceEx(join(config.respath, 'EUROCAPS.TTF'), FR_PRIVATE, 0) + font_path = config.respath_path / 'EUROCAPS.TTF' + AddFontResourceEx(str(font_path), FR_PRIVATE, 0) elif sys.platform == 'linux': # pyright: reportUnboundVariable=false @@ -150,7 +148,7 @@ class _Theme: # the widget has explicit fg or bg attributes. assert isinstance(widget, (tk.BitmapImage, tk.Widget)), widget if not self.defaults: - # Can't initialise this til window is created # Windows, MacOS + # Can't initialise this til window is created # Windows self.defaults = { 'fg': tk.Label()['foreground'], # SystemButtonText, systemButtonText 'bg': tk.Label()['background'], # SystemButtonFace, White @@ -268,8 +266,7 @@ class _Theme: # (Mostly) system colors style = ttk.Style() self.current = { - 'background': (sys.platform == 'darwin' and 'systemMovableModalBackground' or - style.lookup('TLabel', 'background')), + 'background': (style.lookup('TLabel', 'background')), 'foreground': style.lookup('TLabel', 'foreground'), 'activebackground': (sys.platform == 'win32' and 'SystemHighlight' or style.lookup('TLabel', 'background', ['active'])), @@ -292,7 +289,7 @@ class _Theme: # Font only supports Latin 1 / Supplement / Extended, and a # few General Punctuation and Mathematical Operators # LANG: Label for commander name in main window - 'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and + 'font': (theme > 1 and not 0x250 < ord(tr.tl('Cmdr')[0]) < 0x3000 and tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or 'TkDefaultFont'), } @@ -366,8 +363,6 @@ class _Theme: if 'bg' not in attribs: widget['background'] = self.current['background'] widget['activebackground'] = self.current['activebackground'] - if sys.platform == 'darwin' and isinstance(widget, tk.Button): - widget['highlightbackground'] = self.current['background'] if 'font' not in attribs: widget['font'] = self.current['font'] @@ -426,29 +421,8 @@ class _Theme: return # Don't need to mess with the window manager self.active = theme - if sys.platform == 'darwin': - from AppKit import NSAppearance, NSApplication, NSMiniaturizableWindowMask, NSResizableWindowMask - root.update_idletasks() # need main window to be created - if theme == self.THEME_DEFAULT: - appearance = NSAppearance.appearanceNamed_('NSAppearanceNameAqua') - - else: # Dark (Transparent only on win32) - appearance = NSAppearance.appearanceNamed_('NSAppearanceNameDarkAqua') - - for window in NSApplication.sharedApplication().windows(): - window.setStyleMask_(window.styleMask() & ~( - NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom - window.setAppearance_(appearance) - - elif sys.platform == 'win32': - GWL_STYLE = -16 # noqa: N806 # ctypes - WS_MAXIMIZEBOX = 0x00010000 # noqa: N806 # ctypes - # tk8.5.9/win/tkWinWm.c:342 - GWL_EXSTYLE = -20 # noqa: N806 # ctypes - WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes - WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes - GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes - SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes + if sys.platform == 'win32': + import win32con # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: @@ -465,14 +439,17 @@ class _Theme: root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here - hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) - SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize + hwnd = win32gui.GetParent(root.winfo_id()) + win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, + win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) + & ~win32con.WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, + win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar else: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW) # Add to taskbar root.deiconify() root.wait_visibility() # need main window to be displayed before returning diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 24470a93..3d62185d 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -19,23 +19,39 @@ In addition to standard ttk.Label arguments, takes the following arguments: May be imported by plugins """ from __future__ import annotations - +import html +from functools import partial import sys import tkinter as tk import webbrowser from tkinter import font as tk_font from tkinter import ttk -from typing import TYPE_CHECKING, Any +from typing import Any +import plug +from config import config, logger +from l10n import translations as tr +from monitor import monitor -if TYPE_CHECKING: - def _(x: str) -> str: ... +SHIPYARD_HTML_TEMPLATE = """ + + + + + Redirecting you to your {ship_name} at {provider_name}... + + + + You should be redirected to your {ship_name} at {provider_name} shortly... + + + +""" -# FIXME: Split this into multi-file module to separate the platforms -class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # type: ignore +class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore """Clickable label for HTTP links.""" - def __init__(self, master: tk.Frame | None = None, **kw: Any) -> None: + def __init__(self, master: ttk.Frame | tk.Frame | None = None, **kw: Any) -> None: """ Initialize the HyperlinkLabel. @@ -50,22 +66,10 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ self.foreground = kw.get('foreground', 'blue') self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup( 'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option - - if sys.platform == 'darwin': - # Use tk.Label 'cos can't set ttk.Label background - http://www.tkdocs.com/tutorial/styles.html#whydifficult - kw['background'] = kw.pop('background', 'systemDialogBackgroundActive') - kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label - tk.Label.__init__(self, master, **kw) - - else: - ttk.Label.__init__(self, master, **kw) + ttk.Label.__init__(self, master, **kw) self.bind('', self._click) - - self.menu = tk.Menu(tearoff=tk.FALSE) - # LANG: Label for 'Copy' as in 'Copy and Paste' - self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste - self.bind(sys.platform == 'darwin' and '' or '', self._contextmenu) + self.bind('', self._contextmenu) self.bind('', self._enter) self.bind('', self._leave) @@ -75,15 +79,57 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ text=kw.get('text'), font=kw.get('font', ttk.Style().lookup('TLabel', 'font'))) + # Add Menu Options + self.plug_options = kw.pop('plug_options', None) + self.name = kw.get('name', None) + + def open_shipyard(self, url: str): + """Open the Current Ship Loadout in the Selected Provider.""" + if not (loadout := monitor.ship()): + logger.warning('No ship loadout, aborting.') + return '' + if not bool(config.get_int("use_alt_shipyard_open")): + opener = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta) + if opener: + return webbrowser.open(opener) + else: + # Avoid file length limits if possible + target = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta) + file_name = config.app_dir_path / "last_shipyard.html" + + with open(file_name, 'w') as f: + f.write(SHIPYARD_HTML_TEMPLATE.format( + link=html.escape(str(target)), + provider_name=html.escape(str(url)), + ship_name=html.escape("Ship") + )) + + webbrowser.open(f'file://localhost/{file_name}') + + def open_system(self, url: str): + """Open the Current System in the Selected Provider.""" + opener = plug.invoke(url, 'EDSM', 'system_url', monitor.state['SystemName']) + if opener: + return webbrowser.open(opener) + + def open_station(self, url: str): + """Open the Current Station in the Selected Provider.""" + opener = plug.invoke( + url, 'EDSM', 'station_url', + monitor.state['SystemName'], monitor.state['StationName'] + ) + if opener: + return webbrowser.open(opener) + def configure( # noqa: CCR001 self, cnf: dict[str, Any] | None = None, **kw: Any ) -> dict[str, tuple[str, str, str, Any, Any]] | None: """Change cursor and appearance depending on state and text.""" # This class' state - for thing in ['url', 'popup_copy', 'underline']: + for thing in ('url', 'popup_copy', 'underline'): if thing in kw: setattr(self, thing, kw.pop(thing)) - for thing in ['foreground', 'disabledforeground']: + for thing in ('foreground', 'disabledforeground'): if thing in kw: setattr(self, thing, kw[thing]) @@ -106,10 +152,9 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ if state == tk.DISABLED: kw['cursor'] = 'arrow' # System default elif self.url and (kw['text'] if 'text' in kw else self['text']): - kw['cursor'] = 'pointinghand' if sys.platform == 'darwin' else 'hand2' + kw['cursor'] = 'hand2' else: - kw['cursor'] = 'notallowed' if sys.platform == 'darwin' else ( - 'no' if sys.platform == 'win32' else 'circle') + kw['cursor'] = ('no' if sys.platform == 'win32' else 'circle') return super().configure(cnf, **kw) @@ -135,52 +180,55 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ url = self.url(self['text']) if callable(self.url) else self.url if url: self._leave(event) # Remove underline before we change window to browser - openurl(url) + webbrowser.open(url) def _contextmenu(self, event: tk.Event) -> None: + """ + Display the context menu when right-clicked. + + :param event: The event object. + """ + menu = tk.Menu(tearoff=tk.FALSE) + # LANG: Label for 'Copy' as in 'Copy and Paste' + menu.add_command(label=tr.tl('Copy'), command=self.copy) # As in Copy and Paste + + if self.name == 'ship': + # LANG: Copy the Inara SLEF Format of the active ship to the clipboard + menu.add_command(label=tr.tl('Copy Inara SLEF'), command=self.copy_slef, state=tk.DISABLED) + menu.entryconfigure(1, state=monitor.slef and tk.NORMAL or tk.DISABLED) + + menu.add_separator() + for url in plug.provides('shipyard_url'): + menu.add_command( + label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider + command=partial(self.open_shipyard, url) + ) + + if self.name == 'station': + menu.add_separator() + for url in plug.provides('station_url'): + menu.add_command( + label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider + command=partial(self.open_station, url) + ) + + if self.name == 'system': + menu.add_separator() + for url in plug.provides('system_url'): + menu.add_command( + label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider + command=partial(self.open_system, url) + ) + if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy): - self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root) + menu.post(event.x_root, event.y_root) def copy(self) -> None: """Copy the current text to the clipboard.""" self.clipboard_clear() self.clipboard_append(self['text']) - -def openurl(url: str) -> None: - r""" - Open the given URL in appropriate browser. - - 2022-12-06: - Firefox itself will gladly attempt to use very long URLs in its URL - input. Up to 16384 was attempted, but the Apache instance this was - tested against only allowed up to 8207 total URL length to pass, that - being 8190 octets of REQUEST_URI (path + GET params). - - Testing from Windows 10 Home 21H2 cmd.exe with: - - "\firefox.exe" -osint -url "" - - only allowed 8115 octest of REQUEST_URI to pass through. - - Microsoft Edge yielded 8092 octets. Google Chrome yielded 8093 octets. - - However, this is actually the limit of how long a CMD.EXE command-line - can be. The URL was being cut off *there*. - - The 8207 octet URL makes it through `webbrowser.open()` to: - - Firefox 107.0.1 - Microsoft Edge 108.0.1462.42 - Google Chrome 108.0.5359.95 - - This was also tested as working *with* the old winreg/subprocess code, - so it wasn't even suffering from the same limit as CMD.EXE. - - Conclusion: No reason to not just use `webbrowser.open()`, as prior - to e280d6c2833c25867b8139490e68ddf056477917 there was a bug, introduced - in 5989acd0d3263e54429ff99769ff73a20476d863, which meant the code always - ended up using `webbrowser.open()` *anyway*. - :param url: URL to open. - """ - webbrowser.open(url) + def copy_slef(self) -> None: + """Copy the current text to the clipboard.""" + self.clipboard_clear() + self.clipboard_append(monitor.slef) diff --git a/update.py b/update.py index f88daa38..adef9a80 100644 --- a/update.py +++ b/update.py @@ -7,17 +7,18 @@ See LICENSE file. """ from __future__ import annotations -import os +import pathlib +import shutil import sys import threading -from os.path import dirname, join from traceback import print_exc from typing import TYPE_CHECKING from xml.etree import ElementTree import requests import semantic_version -from config import appname, appversion_nobuild, config, update_feed +from config import appname, appversion_nobuild, config, get_update_feed from EDMCLogging import get_main_logger +from l10n import translations as tr if TYPE_CHECKING: import tkinter as tk @@ -26,6 +27,55 @@ if TYPE_CHECKING: logger = get_main_logger() +def check_for_fdev_updates(silent: bool = False, local: bool = False) -> None: # noqa: CCR001 + """Check for and download FDEV ID file updates.""" + if local: + pathway = config.respath_path + else: + pathway = config.app_dir_path + + files_urls = [ + ('commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/commodity.csv'), + ('rare_commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/rare_commodity.csv') + ] + + for file, url in files_urls: + fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file) + fdevid_file.parent.mkdir(parents=True, exist_ok=True) + try: + with open(fdevid_file, newline='', encoding='utf-8') as f: + local_content = f.read() + except FileNotFoundError: + logger.info(f'File {file} not found. Writing from bundle...') + try: + for localfile in files_urls: + filepath = pathlib.Path(f"FDevIDs/{localfile[0]}") + try: + shutil.copy(filepath, pathway / 'FDevIDs') + except shutil.SameFileError: + logger.info("Not replacing same file...") + fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file) + with open(fdevid_file, newline='', encoding='utf-8') as f: + local_content = f.read() + except FileNotFoundError: + local_content = None + + response = requests.get(url, timeout=20) + if response.status_code != 200: + if not silent: + logger.error(f'Failed to download {file}! Unable to continue.') + continue + + if local_content == response.text: + if not silent: + logger.info(f'FDEV ID file {file} already up to date.') + else: + if not silent: + logger.info(f'FDEV ID file {file} not up to date. Downloading...') + with open(fdevid_file, 'w', newline='', encoding='utf-8') as csvfile: + csvfile.write(response.text) + + class EDMCVersion: """ Hold all the information about an EDMC version. @@ -91,7 +141,7 @@ class Updater: self.updater: ctypes.CDLL | None = ctypes.cdll.WinSparkle # Set the appcast URL - self.updater.win_sparkle_set_appcast_url(update_feed.encode()) + self.updater.win_sparkle_set_appcast_url(get_update_feed().encode()) # Set the appversion *without* build metadata, as WinSparkle # doesn't do proper Semantic Version checks. @@ -114,21 +164,6 @@ class Updater: return - if sys.platform == 'darwin': - import objc - - try: - objc.loadBundle( - 'Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework') - ) - # loadBundle presumably supplies `SUUpdater` - self.updater = SUUpdater.sharedUpdater() # noqa: F821 - - except Exception: - # can't load framework - not frozen or not included in app bundle? - print_exc() - self.updater = None - def set_automatic_updates_check(self, onoroff: bool) -> None: """ Set (Win)Sparkle to perform automatic update checks, or not. @@ -141,9 +176,6 @@ class Updater: if sys.platform == 'win32' and self.updater: self.updater.win_sparkle_set_automatic_check_for_updates(onoroff) - if sys.platform == 'darwin' and self.updater: - self.updater.SUEnableAutomaticChecks(onoroff) - def check_for_updates(self) -> None: """Trigger the requisite method to check for an update.""" if self.use_internal(): @@ -154,12 +186,18 @@ class Updater: elif sys.platform == 'win32' and self.updater: self.updater.win_sparkle_check_update_with_ui() - elif sys.platform == 'darwin' and self.updater: - self.updater.checkForUpdates_(None) + check_for_fdev_updates() + # TEMP: Only include until 6.0 + try: + check_for_fdev_updates(local=True) + except Exception as e: + logger.info("Tried to update bundle FDEV files but failed. Don't worry, " + "this likely isn't important and can be ignored unless" + f" you run into other issues. If you're curious: {e}") def check_appcast(self) -> EDMCVersion | None: """ - Manually (no Sparkle or WinSparkle) check the update_feed appcast file. + Manually (no Sparkle or WinSparkle) check the get_update_feed() appcast file. Checks if any listed version is semantically greater than the current running version. @@ -168,7 +206,7 @@ class Updater: newversion = None items = {} try: - request = requests.get(update_feed, timeout=10) + request = requests.get(get_update_feed(), timeout=10) except requests.RequestException as ex: logger.exception(f'Error retrieving update_feed file: {ex}') @@ -183,13 +221,9 @@ class Updater: return None - if sys.platform == 'darwin': - sparkle_platform = 'macos' - - else: - # For *these* purposes anything else is the same as 'windows', as - # non-win32 would be running from source. - sparkle_platform = 'windows' + # For *these* purposes all systems are the same as 'windows', as + # non-win32 would be running from source. + sparkle_platform = 'windows' for item in feed.findall('channel/item'): # xml is a pain with types, hence these ignores @@ -225,7 +259,8 @@ class Updater: if newversion and self.root: status = self.root.nametowidget(f'.{appname.lower()}.status') - status['text'] = newversion.title + ' is available' + # LANG: Update Available Text + status['text'] = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title) self.root.update_idletasks() else: diff --git a/util_ships.py b/util_ships.py index 8bbfd813..b2105106 100644 --- a/util_ships.py +++ b/util_ships.py @@ -5,18 +5,21 @@ Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. """ +from pathlib import Path from edmc_data import ship_name_map def ship_file_name(ship_name: str, ship_type: str) -> str: """Return a ship name suitable for a filename.""" name = str(ship_name or ship_name_map.get(ship_type.lower(), ship_type)).strip() - if name.endswith('.'): - name = name[:-2] - if name.lower() in ('con', 'prn', 'aux', 'nul', - 'com0', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', - 'lpt0', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'): - name += '_' + # Handle suffix using Pathlib's with_suffix method + name = Path(name).with_suffix("").name - return name.translate({ord(x): '_' for x in ('\0', '<', '>', ':', '"', '/', '\\', '|', '?', '*')}) + # Check if the name is a reserved filename + if Path(name).is_reserved(): + name += "_" + + return name.translate( + {ord(x): "_" for x in ("\0", "<", ">", ":", '"', "/", "\\", "|", "?", "*")} + )