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