1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-17 17:42:20 +03:00

Merge pull request #1150 from A-UNDERSCORE-D/enhancement/ast-lang-update

Added AST based LANG translation string detection
This commit is contained in:
Athanasius 2021-06-07 12:01:55 +01:00 committed by GitHub
commit f80130a1de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 498 additions and 152 deletions

View File

@ -148,7 +148,7 @@ your WIP branch. If there are any non-trivial conflicts when we merge your Pull
to rebase your WIP branch on the latest version of the source branch. Otherwise we'll work out how to best
merge your changes via comments in the Pull Request.
### Linting
## Linting
We use flake8 for linting all python source.
@ -158,7 +158,7 @@ your files.
Note that if your PR does not cleanly (or mostly cleanly) pass a linting scan, your PR may be put on hold pending fixes.
### Unit testing
## Unit testing
Where possible please write unit tests for your PRs, especially in the case of
bug fixes, having regression tests help ensure that we don't accidentally
@ -362,11 +362,26 @@ literals.
Doc strings are preferred on all new modules, functions, classes, and methods, as they help others understand your code.
We use the `sphinx` formatting style, which for pycharm users is the default.
## Mark hacks and workarounds with a specific comment
## Comments
### Add comments to LANG usage
Sometimes our translators may need some additional information about what a translation is used for. You can add
that information automatically by using `# LANG: your message here`
**on the line directly above your usage, or at the end of the line in your usage**. If both comments exist, the one
on the current line is preferred over the one above
```py
# LANG: this says stuff.
_('stuff')
```
### Mark hacks and workarounds with a specific comment
We often write hacks or workarounds to make EDMC work on a given version or around a specific bug.
Please mark all hacks, workarounds, magic with one of the following comments, where applicable:
```
```py
# HACK $elite-version-number | $date: $description
# MAGIC $elite-version-number | $date: $description
# WORKAROUND $elite-version-number | $date: $description

View File

@ -414,7 +414,7 @@ class AppWindow(object):
else:
appitem.grid(columnspan=2, sticky=tk.EW)
# Update button in main window
# LANG: Update button in main window
self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED)
self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED)
self.status = tk.Label(frame, name='status', anchor=tk.W)
@ -626,7 +626,7 @@ class AppWindow(object):
return
if (suit := monitor.state.get('SuitCurrent')) is None:
self.suit['text'] = f'<{_("Unknown")}>'
self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit
return
suitname = suit['edmcName']
@ -701,48 +701,49 @@ class AppWindow(object):
def set_labels(self):
"""Set main window labels, e.g. after language change."""
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Main window
# Multicrew role label in main window
self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window
self.suit_label['text'] = _('Suit') + ':' # Main window
self.system_label['text'] = _('System') + ':' # Main window
self.station_label['text'] = _('Station') + ':' # Main window
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
self.suit_label['text'] = _('Suit') + ':' # LANG: Main window
self.system_label['text'] = _('System') + ':' # LANG: Main window
self.station_label['text'] = _('Station') + ':' # LANG: Main window
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
if platform == 'darwin':
self.menubar.entryconfigure(1, label=_('File')) # Menu title
self.menubar.entryconfigure(2, label=_('Edit')) # Menu title
self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX
self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX
self.menubar.entryconfigure(5, label=_('Help')) # Menu title
self.system_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX
self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item
self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item
self.view_menu.entryconfigure(0, label=_('Status')) # Menu item
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item
self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title on OSX
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title on OSX
self.menubar.entryconfigure(3, label=_('View')) # LANG: Menu title on OSX
self.menubar.entryconfigure(4, label=_('Window')) # LANG: Menu title on OSX
self.menubar.entryconfigure(5, label=_('Help')) # LANG: Menu title on OSX
self.system_menu.entryconfigure(0, label=_("About {APP}").format(
APP=applongname)) # LANG: App menu entry on OSX
self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Menu item
self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: Menu item
self.view_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item
else:
self.menubar.entryconfigure(1, label=_('File')) # Menu title
self.menubar.entryconfigure(2, label=_('Edit')) # Menu title
self.menubar.entryconfigure(3, label=_('Help')) # Menu title
self.theme_file_menu['text'] = _('File') # Menu title
self.theme_edit_menu['text'] = _('Edit') # Menu title
self.theme_help_menu['text'] = _('Help') # Menu title
self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title
self.menubar.entryconfigure(3, label=_('Help')) # LANG: Menu title
self.theme_file_menu['text'] = _('File') # LANG: Menu title
self.theme_edit_menu['text'] = _('Edit') # LANG: Menu title
self.theme_help_menu['text'] = _('Help') # LANG: Menu title
# File menu
self.file_menu.entryconfigure(0, label=_('Status')) # Menu item
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item
self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows
self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows
self.file_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: Menu item
self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: Item in the File menu on Windows
self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: Item in the File menu on Windows
# Help menu
self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item
self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item
self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry
self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help menu item
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item
self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # LANG: Menu item
self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # LANG: App menu entry
# Edit menu
self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste
self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: As in Copy and Paste
def login(self):
"""Initiate CAPI/Frontier login and set other necessary state."""
@ -1004,9 +1005,9 @@ class AppWindow(object):
return {
None: '',
'Idle': '',
'FighterCon': _('Fighter'), # Multicrew role
'FireCon': _('Gunner'), # Multicrew role
'FlightCon': _('Helm'), # Multicrew role
'FighterCon': _('Fighter'), # LANG: Multicrew role
'FireCon': _('Gunner'), # LANG: Multicrew role
'FlightCon': _('Helm'), # LANG: Multicrew role
}.get(role, role)
if monitor.thread is None:
@ -1024,7 +1025,7 @@ class AppWindow(object):
self.cooldown()
if monitor.cmdr and monitor.state['Captain']:
self.cmdr['text'] = f'{monitor.cmdr} / {monitor.state["Captain"]}'
self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window
self.ship_label['text'] = _('Role') + ':' # LANG: Multicrew role label in main window
self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None)
elif monitor.cmdr:
@ -1034,7 +1035,7 @@ class AppWindow(object):
else:
self.cmdr['text'] = monitor.cmdr
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship_label['text'] = _('Ship') + ':' # LANG: Main window
# TODO: Show something else when on_foot
if monitor.state['ShipName']:
@ -1057,7 +1058,7 @@ class AppWindow(object):
else:
self.cmdr['text'] = ''
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship_label['text'] = _('Ship') + ':' # LANG: Main window
self.ship['text'] = ''
if monitor.cmdr and monitor.is_beta:
@ -1166,7 +1167,7 @@ class AppWindow(object):
"""
try:
companion.session.auth_callback()
# Successfully authenticated with the Frontier website
# LANG: Successfully authenticated with the Frontier website
self.status['text'] = _('Authentication successful')
if platform == 'darwin':
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
@ -1254,7 +1255,7 @@ class AppWindow(object):
self.w.after(1000, self.cooldown)
else:
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
self.button['state'] = self.theme_button['state'] = (monitor.cmdr and
monitor.mode and
not monitor.state['Captain'] and
@ -1721,18 +1722,16 @@ sys.path: {sys.path}'''
"""Display message about plugins not updated for Python 3.x."""
plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0)
if (plugins_not_py3_last + 86400) < int(time()) and len(plug.PLUGINS_not_py3):
# Yes, this is horribly hacky so as to be sure we match the key
# that we told Translators to use.
popup_text = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the " \
"list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an " \
"updated version available, else alert the developer that they need to update the code for " \
"Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on " \
"the end of the name."
popup_text = popup_text.replace('\n', '\\n')
popup_text = popup_text.replace('\r', '\\r')
# Now the string should match, so try translation
popup_text = _(popup_text)
# And substitute in the other words.
popup_text = _(
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the "
"list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an "
"updated version available, else alert the developer that they need to update the code for "
r"Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on "
"the end of the name."
)
# Substitute in the other words.
# LANG: words for use in python 2 plugin error
popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'),
DISABLED='.disabled')
# And now we do need these to be actual \r\n

View File

@ -0,0 +1,326 @@
"""Search all given paths recursively for localised string calls."""
import argparse
import ast
import dataclasses
import json
import pathlib
import re
import sys
from typing import Optional
# spell-checker: words dedupe deduping deduped
def get_func_name(thing: ast.AST) -> str:
"""Get the name of a function from a Call node."""
if isinstance(thing, ast.Name):
return thing.id
elif isinstance(thing, ast.Attribute):
return get_func_name(thing.value)
else:
return ''
def get_arg(call: ast.Call) -> str:
"""Extract the argument string to the translate function."""
if len(call.args) > 1:
print('??? > 1 args', call.args, file=sys.stderr)
arg = call.args[0]
if isinstance(arg, ast.Constant):
return arg.value
elif isinstance(arg, ast.Name):
return f'VARIABLE! CHECK CODE! {arg.id}'
else:
return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}'
def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]:
"""Recursively find ast.Calls in a statement."""
out = []
for n in ast.iter_child_nodes(statement):
out.extend(find_calls_in_stmt(n))
if isinstance(statement, ast.Call) and get_func_name(statement.func) == '_':
out.append(statement)
return out
COMMENT_RE = re.compile(r'^.*?(#.*)$')
def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> Optional[str]:
"""
Extract comments from source code based on the given call.
This returns comments on the same line as the call preferentially to comments above.
All comments must be prefixed with LANG, ie `# LANG: `
:param call: The call node to look for comments around.
:param lines: The file that the call node came from, as a list of strings where each string is a line.
:param file: The path to the file this call node came from
:return: The first comment that matches the rules, or None
"""
out: list[Optional[str]] = []
above = call.lineno - 2
current = call.lineno - 1
above_line = lines[above].strip() if len(lines) < above else None
current_line = lines[current].strip()
for line in (above_line, current_line):
if line is None or '#' not in line:
out.append(None)
continue
match = COMMENT_RE.match(line)
if not match:
print(line)
out.append(None)
continue
comment = match.group(1).strip()
if not comment.startswith('# LANG:'):
print(f'Unknown comment for {file}:{current} {line}', file=sys.stderr)
out.append(None)
continue
out.append(comment.replace('# LANG:', '').strip())
if out[1] is not None:
return out[1]
elif out[0] is not None:
return out[0]
return None
def scan_file(path: pathlib.Path) -> list[ast.Call]:
"""Scan a file for ast.Calls."""
data = path.read_text(encoding='utf-8')
lines = data.splitlines()
parsed = ast.parse(data)
out: list[ast.Call] = []
for statement in parsed.body:
out.extend(find_calls_in_stmt(statement))
# see if we can extract any comments
for call in out:
setattr(call, 'comment', extract_comments(call, lines, path))
out.sort(key=lambda c: c.lineno)
return out
def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] = None) -> dict[pathlib.Path, list[ast.Call]]:
"""
Scan a directory for expected callsites.
:param path: path to scan
:param skip: paths to skip, if any, defaults to None
"""
out = {}
for thing in path.iterdir():
if skip is not None and any(s.name == thing.name for s in skip):
continue
if thing.is_file():
if not thing.name.endswith('.py'):
continue
out[thing] = scan_file(thing)
elif thing.is_dir():
out |= scan_directory(thing)
else:
raise ValueError(type(thing), thing)
return out
def parse_template(path) -> set[str]:
"""
Parse a lang.template file.
The regexp this uses was extracted from l10n.py.
:param path: The path to the lang file
"""
lang_re = re.compile(r'\s*"((?:[^"]|(?:\"))+)"\s*=\s*"((?:[^"]|(?:\"))+)"\s*;\s*$')
out = set()
for line in pathlib.Path(path).read_text(encoding='utf-8').splitlines():
match = lang_re.match(line)
if not match:
continue
if match.group(1) != '!Language':
out.add(match.group(1))
return out
@dataclasses.dataclass
class FileLocation:
"""FileLocation is the location of a given string in a file."""
path: pathlib.Path
line_start: int
line_start_col: int
line_end: Optional[int]
line_end_col: Optional[int]
@staticmethod
def from_call(path: pathlib.Path, c: ast.Call) -> 'FileLocation':
"""
Create a FileLocation from a Call and Path.
:param path: Path to the file this FileLocation is in
:param c: Call object to extract line information from
"""
return FileLocation(path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset)
@dataclasses.dataclass
class LangEntry:
"""LangEntry is a single translation that may span multiple files or locations."""
locations: list[FileLocation]
string: str
comments: list[Optional[str]]
def files(self) -> str:
"""Return a string representation of all the files this LangEntry is in, and its location therein."""
out = ''
for loc in self.locations:
start = loc.line_start
end = loc.line_end
end_str = f':{end}' if end is not None and end != start else ''
out += f'{loc.path.name}:{start}{end_str}; '
return out
def dedupe_lang_entries(entries: list[LangEntry]) -> list[LangEntry]:
"""
Deduplicate a list of lang entries.
This will coalesce LangEntries that have that same string but differing files and comments into a single
LangEntry that cotains all comments and FileLocations
:param entries: The list to deduplicate
:return: The deduplicated list
"""
deduped: list[LangEntry] = []
for e in entries:
cont = False
for d in deduped:
if d.string == e.string:
cont = True
d.locations.append(e.locations[0])
d.comments.extend(e.comments)
if cont:
continue
deduped.append(e)
return deduped
def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str:
"""Generate a full en.template from the given data."""
entries: list[LangEntry] = []
for path, calls in data.items():
for c in calls:
entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')]))
deduped = dedupe_lang_entries(entries)
out = ''
print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr)
for entry in deduped:
assert len(entry.comments) == len(entry.locations)
comment = ''
files = 'In files: ' + entry.files()
string = f'"{entry.string}"'
for i in range(len(entry.comments)):
if entry.comments[i] is None:
continue
loc = entry.locations[i]
to_append = f'{loc.path.name}: {entry.comments[i]}; '
if to_append not in comment:
comment += to_append
header = f'{comment.strip()} {files}'.strip()
out += f'/* {header} */\n'
out += f'{string} = {string};\n'
out += '\n'
return out
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--directory', help='Directory to search from', default='.')
parser.add_argument('--ignore', action='append', help='directories to ignore', default=['venv', '.git'])
group = parser.add_mutually_exclusive_group()
group.add_argument('--json', action='store_true', help='JSON output')
group.add_argument('--lang', action='store_true', help='lang file outpot')
group.add_argument('--compare-lang')
args = parser.parse_args()
directory = pathlib.Path(args.directory)
res = scan_directory(directory, [pathlib.Path(p) for p in args.ignore])
if args.compare_lang is not None and len(args.compare_lang) > 0:
seen = set()
template = parse_template(args.compare_lang)
for file, calls in res.items():
for c in calls:
arg = get_arg(c)
if arg in template:
seen.add(arg)
else:
print(f'NEW! {file}:{c.lineno}: {arg!r}')
for old in set(template) ^ seen:
print(f'No longer used: {old}')
elif args.json:
to_print_data = [
{
'path': str(path),
'string': get_arg(c),
'reconstructed': ast.unparse(c),
'start_line': c.lineno,
'start_offset': c.col_offset,
'end_line': c.end_lineno,
'end_offset': c.end_col_offset,
'comment': getattr(c, 'comment', None)
} for (path, calls) in res.items() for c in calls
]
print(json.dumps(to_print_data, indent=2))
elif args.lang:
print(generate_lang_template(res))
else:
for path, calls in res.items():
if len(calls) == 0:
continue
print(path)
for c in calls:
print(
f' {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t', ast.unparse(c)
)
print()

194
stats.py
View File

@ -47,117 +47,117 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
"""
# StatsResults assumes these three things are first
res = [
[_('Cmdr'), data['commander']['name']],
[_('Balance'), str(data['commander'].get('credits', 0))], # Cmdr stats
[_('Loan'), str(data['commander'].get('debt', 0))], # Cmdr stats
[_('Cmdr'), data['commander']['name']], # LANG: Cmdr stats
[_('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats
[_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats
]
RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime
# in output order
(_('Combat'), 'combat'), # Ranking
(_('Trade'), 'trade'), # Ranking
(_('Explorer'), 'explore'), # Ranking
(_('CQC'), 'cqc'), # Ranking
(_('Federation'), 'federation'), # Ranking
(_('Empire'), 'empire'), # Ranking
(_('Powerplay'), 'power'), # Ranking
# ??? , 'crime'), # Ranking
# ??? , 'service'), # Ranking
(_('Combat'), 'combat'), # LANG: Ranking
(_('Trade'), 'trade'), # LANG: Ranking
(_('Explorer'), 'explore'), # LANG: Ranking
(_('CQC'), 'cqc'), # LANG: Ranking
(_('Federation'), 'federation'), # LANG: Ranking
(_('Empire'), 'empire'), # LANG: Ranking
(_('Powerplay'), 'power'), # LANG: Ranking
# ??? , 'crime'), # LANG: Ranking
# ??? , 'service'), # LANG: Ranking
]
RANK_NAMES = { # noqa: N806 # Its a constant, just needs to be updated at runtime
# http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks
'combat': [
_('Harmless'), # Combat rank
_('Mostly Harmless'), # Combat rank
_('Novice'), # Combat rank
_('Competent'), # Combat rank
_('Expert'), # Combat rank
_('Master'), # Combat rank
_('Dangerous'), # Combat rank
_('Deadly'), # Combat rank
_('Elite'), # Top rank
_('Harmless'), # LANG: Combat rank
_('Mostly Harmless'), # LANG: Combat rank
_('Novice'), # LANG: Combat rank
_('Competent'), # LANG: Combat rank
_('Expert'), # LANG: Combat rank
_('Master'), # LANG: Combat rank
_('Dangerous'), # LANG: Combat rank
_('Deadly'), # LANG: Combat rank
_('Elite'), # LANG: Top rank
],
'trade': [
_('Penniless'), # Trade rank
_('Mostly Penniless'), # Trade rank
_('Peddler'), # Trade rank
_('Dealer'), # Trade rank
_('Merchant'), # Trade rank
_('Broker'), # Trade rank
_('Entrepreneur'), # Trade rank
_('Tycoon'), # Trade rank
_('Elite') # Top rank
_('Penniless'), # LANG: Trade rank
_('Mostly Penniless'), # LANG: Trade rank
_('Peddler'), # LANG: Trade rank
_('Dealer'), # LANG: Trade rank
_('Merchant'), # LANG: Trade rank
_('Broker'), # LANG: Trade rank
_('Entrepreneur'), # LANG: Trade rank
_('Tycoon'), # LANG: Trade rank
_('Elite') # LANG: Top rank
],
'explore': [
_('Aimless'), # Explorer rank
_('Mostly Aimless'), # Explorer rank
_('Scout'), # Explorer rank
_('Surveyor'), # Explorer rank
_('Trailblazer'), # Explorer rank
_('Pathfinder'), # Explorer rank
_('Ranger'), # Explorer rank
_('Pioneer'), # Explorer rank
_('Elite') # Top rank
_('Aimless'), # LANG: Explorer rank
_('Mostly Aimless'), # LANG: Explorer rank
_('Scout'), # LANG: Explorer rank
_('Surveyor'), # LANG: Explorer rank
_('Trailblazer'), # LANG: Explorer rank
_('Pathfinder'), # LANG: Explorer rank
_('Ranger'), # LANG: Explorer rank
_('Pioneer'), # LANG: Explorer rank
_('Elite') # LANG: Top rank
],
'cqc': [
_('Helpless'), # CQC rank
_('Mostly Helpless'), # CQC rank
_('Amateur'), # CQC rank
_('Semi Professional'), # CQC rank
_('Professional'), # CQC rank
_('Champion'), # CQC rank
_('Hero'), # CQC rank
_('Gladiator'), # CQC rank
_('Elite') # Top rank
_('Helpless'), # LANG: CQC rank
_('Mostly Helpless'), # LANG: CQC rank
_('Amateur'), # LANG: CQC rank
_('Semi Professional'), # LANG: CQC rank
_('Professional'), # LANG: CQC rank
_('Champion'), # LANG: CQC rank
_('Hero'), # LANG: CQC rank
_('Gladiator'), # LANG: CQC rank
_('Elite') # LANG: Top rank
],
# http://elite-dangerous.wikia.com/wiki/Federation#Ranks
'federation': [
_('None'), # No rank
_('Recruit'), # Federation rank
_('Cadet'), # Federation rank
_('Midshipman'), # Federation rank
_('Petty Officer'), # Federation rank
_('Chief Petty Officer'), # Federation rank
_('Warrant Officer'), # Federation rank
_('Ensign'), # Federation rank
_('Lieutenant'), # Federation rank
_('Lieutenant Commander'), # Federation rank
_('Post Commander'), # Federation rank
_('Post Captain'), # Federation rank
_('Rear Admiral'), # Federation rank
_('Vice Admiral'), # Federation rank
_('Admiral') # Federation rank
_('None'), # LANG: No rank
_('Recruit'), # LANG: Federation rank
_('Cadet'), # LANG: Federation rank
_('Midshipman'), # LANG: Federation rank
_('Petty Officer'), # LANG: Federation rank
_('Chief Petty Officer'), # LANG: Federation rank
_('Warrant Officer'), # LANG: Federation rank
_('Ensign'), # LANG: Federation rank
_('Lieutenant'), # LANG: Federation rank
_('Lieutenant Commander'), # LANG: Federation rank
_('Post Commander'), # LANG: Federation rank
_('Post Captain'), # LANG: Federation rank
_('Rear Admiral'), # LANG: Federation rank
_('Vice Admiral'), # LANG: Federation rank
_('Admiral') # LANG: Federation rank
],
# http://elite-dangerous.wikia.com/wiki/Empire#Ranks
'empire': [
_('None'), # No rank
_('Outsider'), # Empire rank
_('Serf'), # Empire rank
_('Master'), # Empire rank
_('Squire'), # Empire rank
_('Knight'), # Empire rank
_('Lord'), # Empire rank
_('Baron'), # Empire rank
_('Viscount'), # Empire rank
_('Count'), # Empire rank
_('Earl'), # Empire rank
_('Marquis'), # Empire rank
_('Duke'), # Empire rank
_('Prince'), # Empire rank
_('King') # Empire rank
_('None'), # LANG: No rank
_('Outsider'), # LANG: Empire rank
_('Serf'), # LANG: Empire rank
_('Master'), # LANG: Empire rank
_('Squire'), # LANG: Empire rank
_('Knight'), # LANG: Empire rank
_('Lord'), # LANG: Empire rank
_('Baron'), # LANG: Empire rank
_('Viscount'), # LANG: Empire rank
_('Count'), # LANG: Empire rank
_('Earl'), # LANG: Empire rank
_('Marquis'), # LANG: Empire rank
_('Duke'), # LANG: Empire rank
_('Prince'), # LANG: Empire rank
_('King') # LANG: Empire rank
],
# http://elite-dangerous.wikia.com/wiki/Ratings
'power': [
_('None'), # No rank
_('Rating 1'), # Power rank
_('Rating 2'), # Power rank
_('Rating 3'), # Power rank
_('Rating 4'), # Power rank
_('Rating 5') # Power rank
_('None'), # LANG: No rank
_('Rating 1'), # LANG: Power rank
_('Rating 2'), # LANG: Power rank
_('Rating 3'), # LANG: Power rank
_('Rating 4'), # LANG: Power rank
_('Rating 5') # LANG: Power rank
],
}
@ -169,7 +169,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
res.append([title, names[rank] if rank < len(names) else f'Rank {rank}'])
else:
res.append([title, _('None')]) # No rank
res.append([title, _('None')]) # LANG: No rank
return res
@ -291,7 +291,9 @@ class StatsDialog():
return
if not data.get('commander') or not data['commander'].get('name', '').strip():
self.status['text'] = _("Who are you?!") # Shouldn't happen
# Shouldn't happen
# LANG: Unknown commander
self.status['text'] = _("Who are you?!")
elif (
not data.get('lastSystem')
@ -299,10 +301,14 @@ class StatsDialog():
or not data.get('lastStarport')
or not data['lastStarport'].get('name', '').strip()
):
self.status['text'] = _("Where are you?!") # Shouldn't happen
# Shouldn't happen
# LANG: Unknown location
self.status['text'] = _("Where are you?!")
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip():
self.status['text'] = _("What are you flying?!") # Shouldn't happen
# Shouldn't happen
# LANG: Unknown ship
self.status['text'] = _("What are you flying?!")
else:
self.status['text'] = ''
@ -350,14 +356,14 @@ class StatsResults(tk.Toplevel):
self.addpagerow(page, thing, with_copy=True)
ttk.Frame(page).grid(pady=5) # bottom spacer
notebook.add(page, text=_('Status')) # Status dialog title
notebook.add(page, text=_('Status')) # LANG: Status dialog title
page = self.addpage(notebook, [
_('Ship'), # Status dialog subtitle
_('Ship'), # LANG: Status dialog subtitle
'',
_('System'), # Main window
_('Station'), # Status dialog subtitle
_('Value'), # Status dialog subtitle - CR value of ship
_('System'), # LANG: Main window
_('Station'), # LANG: Status dialog subtitle
_('Value'), # LANG: Status dialog subtitle - CR value of ship
])
shiplist = ships(data)
@ -366,7 +372,7 @@ class StatsResults(tk.Toplevel):
self.addpagerow(page, list(ship_data[1:-1]) + [self.credits(int(ship_data[-1]))], with_copy=True)
ttk.Frame(page).grid(pady=5) # bottom spacer
notebook.add(page, text=_('Ships')) # Status dialog title
notebook.add(page, text=_('Ships')) # LANG: Status dialog title
if platform != 'darwin':
buttonframe = ttk.Frame(frame)