diff --git a/Contributing.md b/Contributing.md index 3a296798..4dc629ec 100644 --- a/Contributing.md +++ b/Contributing.md @@ -148,7 +148,7 @@ your WIP branch. If there are any non-trivial conflicts when we merge your Pull to rebase your WIP branch on the latest version of the source branch. Otherwise we'll work out how to best merge your changes via comments in the Pull Request. -### Linting +## Linting We use flake8 for linting all python source. @@ -158,7 +158,7 @@ your files. Note that if your PR does not cleanly (or mostly cleanly) pass a linting scan, your PR may be put on hold pending fixes. -### Unit testing +## Unit testing Where possible please write unit tests for your PRs, especially in the case of bug fixes, having regression tests help ensure that we don't accidentally @@ -362,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 diff --git a/EDMarketConnector.py b/EDMarketConnector.py index d757c905..4fc058ff 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -414,7 +414,7 @@ class AppWindow(object): else: appitem.grid(columnspan=2, sticky=tk.EW) - # Update button in main window + # LANG: Update button in main window self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) @@ -626,7 +626,7 @@ class AppWindow(object): return if (suit := monitor.state.get('SuitCurrent')) is None: - self.suit['text'] = f'<{_("Unknown")}>' + self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit return suitname = suit['edmcName'] @@ -701,48 +701,49 @@ class AppWindow(object): def set_labels(self): """Set main window labels, e.g. after language change.""" - self.cmdr_label['text'] = _('Cmdr') + ':' # Main window + self.cmdr_label['text'] = _('Cmdr') + ':' # LANG: Main window # Multicrew role label in main window self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window - self.suit_label['text'] = _('Suit') + ':' # Main window - self.system_label['text'] = _('System') + ':' # Main window - self.station_label['text'] = _('Station') + ':' # Main window - self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window + self.suit_label['text'] = _('Suit') + ':' # LANG: Main window + self.system_label['text'] = _('System') + ':' # LANG: Main window + self.station_label['text'] = _('Station') + ':' # LANG: Main window + self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window if platform == 'darwin': - self.menubar.entryconfigure(1, label=_('File')) # Menu title - self.menubar.entryconfigure(2, label=_('Edit')) # Menu title - self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX - self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX - self.menubar.entryconfigure(5, label=_('Help')) # Menu title - self.system_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX - self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item - self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item - self.view_menu.entryconfigure(0, label=_('Status')) # Menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item + self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title on OSX + self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title on OSX + self.menubar.entryconfigure(3, label=_('View')) # LANG: Menu title on OSX + self.menubar.entryconfigure(4, label=_('Window')) # LANG: Menu title on OSX + self.menubar.entryconfigure(5, label=_('Help')) # LANG: Menu title on OSX + self.system_menu.entryconfigure(0, label=_("About {APP}").format( + APP=applongname)) # LANG: App menu entry on OSX + self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Menu item + self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: Menu item + self.view_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item else: - self.menubar.entryconfigure(1, label=_('File')) # Menu title - self.menubar.entryconfigure(2, label=_('Edit')) # Menu title - self.menubar.entryconfigure(3, label=_('Help')) # Menu title - self.theme_file_menu['text'] = _('File') # Menu title - self.theme_edit_menu['text'] = _('Edit') # Menu title - self.theme_help_menu['text'] = _('Help') # Menu title + self.menubar.entryconfigure(1, label=_('File')) # LANG: Menu title + self.menubar.entryconfigure(2, label=_('Edit')) # LANG: Menu title + self.menubar.entryconfigure(3, label=_('Help')) # LANG: Menu title + self.theme_file_menu['text'] = _('File') # LANG: Menu title + self.theme_edit_menu['text'] = _('Edit') # LANG: Menu title + self.theme_help_menu['text'] = _('Help') # LANG: Menu title # File menu - self.file_menu.entryconfigure(0, label=_('Status')) # Menu item - self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item - self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows - self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows + self.file_menu.entryconfigure(0, label=_('Status')) # LANG: Menu item + self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: Menu item + self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: Item in the File menu on Windows + self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: Item in the File menu on Windows # Help menu - self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item - self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item - self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item - self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item - self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry + self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help menu item + self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # LANG: Help menu item + self.help_menu.entryconfigure(2, label=_('Release Notes')) # LANG: Help menu item + self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # LANG: Menu item + self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # LANG: App menu entry # Edit menu - self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste + self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: As in Copy and Paste def login(self): """Initiate CAPI/Frontier login and set other necessary state.""" @@ -1004,9 +1005,9 @@ class AppWindow(object): return { None: '', 'Idle': '', - 'FighterCon': _('Fighter'), # Multicrew role - 'FireCon': _('Gunner'), # Multicrew role - 'FlightCon': _('Helm'), # Multicrew role + 'FighterCon': _('Fighter'), # LANG: Multicrew role + 'FireCon': _('Gunner'), # LANG: Multicrew role + 'FlightCon': _('Helm'), # LANG: Multicrew role }.get(role, role) if monitor.thread is None: @@ -1024,7 +1025,7 @@ class AppWindow(object): self.cooldown() if monitor.cmdr and monitor.state['Captain']: self.cmdr['text'] = f'{monitor.cmdr} / {monitor.state["Captain"]}' - self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window + self.ship_label['text'] = _('Role') + ':' # LANG: Multicrew role label in main window self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None) elif monitor.cmdr: @@ -1034,7 +1035,7 @@ class AppWindow(object): else: self.cmdr['text'] = monitor.cmdr - self.ship_label['text'] = _('Ship') + ':' # Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: Main window # TODO: Show something else when on_foot if monitor.state['ShipName']: @@ -1057,7 +1058,7 @@ class AppWindow(object): else: self.cmdr['text'] = '' - self.ship_label['text'] = _('Ship') + ':' # Main window + self.ship_label['text'] = _('Ship') + ':' # LANG: Main window self.ship['text'] = '' if monitor.cmdr and monitor.is_beta: @@ -1166,7 +1167,7 @@ class AppWindow(object): """ try: companion.session.auth_callback() - # Successfully authenticated with the Frontier website + # LANG: Successfully authenticated with the Frontier website self.status['text'] = _('Authentication successful') if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status @@ -1254,7 +1255,7 @@ class AppWindow(object): self.w.after(1000, self.cooldown) else: - self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window + self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window self.button['state'] = self.theme_button['state'] = (monitor.cmdr and monitor.mode and not monitor.state['Captain'] and @@ -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 diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py new file mode 100644 index 00000000..b62ddd78 --- /dev/null +++ b/scripts/find_localised_strings.py @@ -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() diff --git a/stats.py b/stats.py index 704e4703..2eab96f2 100644 --- a/stats.py +++ b/stats.py @@ -47,117 +47,117 @@ def status(data: Dict[str, Any]) -> List[List[str]]: """ # StatsResults assumes these three things are first res = [ - [_('Cmdr'), data['commander']['name']], - [_('Balance'), str(data['commander'].get('credits', 0))], # Cmdr stats - [_('Loan'), str(data['commander'].get('debt', 0))], # Cmdr stats + [_('Cmdr'), data['commander']['name']], # LANG: Cmdr stats + [_('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats + [_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats ] RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime # in output order - (_('Combat'), 'combat'), # Ranking - (_('Trade'), 'trade'), # Ranking - (_('Explorer'), 'explore'), # Ranking - (_('CQC'), 'cqc'), # Ranking - (_('Federation'), 'federation'), # Ranking - (_('Empire'), 'empire'), # Ranking - (_('Powerplay'), 'power'), # Ranking - # ??? , 'crime'), # Ranking - # ??? , 'service'), # Ranking + (_('Combat'), 'combat'), # LANG: Ranking + (_('Trade'), 'trade'), # LANG: Ranking + (_('Explorer'), 'explore'), # LANG: Ranking + (_('CQC'), 'cqc'), # LANG: Ranking + (_('Federation'), 'federation'), # LANG: Ranking + (_('Empire'), 'empire'), # LANG: Ranking + (_('Powerplay'), 'power'), # LANG: Ranking + # ??? , 'crime'), # LANG: Ranking + # ??? , 'service'), # LANG: Ranking ] RANK_NAMES = { # noqa: N806 # Its a constant, just needs to be updated at runtime # http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks 'combat': [ - _('Harmless'), # Combat rank - _('Mostly Harmless'), # Combat rank - _('Novice'), # Combat rank - _('Competent'), # Combat rank - _('Expert'), # Combat rank - _('Master'), # Combat rank - _('Dangerous'), # Combat rank - _('Deadly'), # Combat rank - _('Elite'), # Top rank + _('Harmless'), # LANG: Combat rank + _('Mostly Harmless'), # LANG: Combat rank + _('Novice'), # LANG: Combat rank + _('Competent'), # LANG: Combat rank + _('Expert'), # LANG: Combat rank + _('Master'), # LANG: Combat rank + _('Dangerous'), # LANG: Combat rank + _('Deadly'), # LANG: Combat rank + _('Elite'), # LANG: Top rank ], 'trade': [ - _('Penniless'), # Trade rank - _('Mostly Penniless'), # Trade rank - _('Peddler'), # Trade rank - _('Dealer'), # Trade rank - _('Merchant'), # Trade rank - _('Broker'), # Trade rank - _('Entrepreneur'), # Trade rank - _('Tycoon'), # Trade rank - _('Elite') # Top rank + _('Penniless'), # LANG: Trade rank + _('Mostly Penniless'), # LANG: Trade rank + _('Peddler'), # LANG: Trade rank + _('Dealer'), # LANG: Trade rank + _('Merchant'), # LANG: Trade rank + _('Broker'), # LANG: Trade rank + _('Entrepreneur'), # LANG: Trade rank + _('Tycoon'), # LANG: Trade rank + _('Elite') # LANG: Top rank ], 'explore': [ - _('Aimless'), # Explorer rank - _('Mostly Aimless'), # Explorer rank - _('Scout'), # Explorer rank - _('Surveyor'), # Explorer rank - _('Trailblazer'), # Explorer rank - _('Pathfinder'), # Explorer rank - _('Ranger'), # Explorer rank - _('Pioneer'), # Explorer rank - _('Elite') # Top rank + _('Aimless'), # LANG: Explorer rank + _('Mostly Aimless'), # LANG: Explorer rank + _('Scout'), # LANG: Explorer rank + _('Surveyor'), # LANG: Explorer rank + _('Trailblazer'), # LANG: Explorer rank + _('Pathfinder'), # LANG: Explorer rank + _('Ranger'), # LANG: Explorer rank + _('Pioneer'), # LANG: Explorer rank + _('Elite') # LANG: Top rank ], 'cqc': [ - _('Helpless'), # CQC rank - _('Mostly Helpless'), # CQC rank - _('Amateur'), # CQC rank - _('Semi Professional'), # CQC rank - _('Professional'), # CQC rank - _('Champion'), # CQC rank - _('Hero'), # CQC rank - _('Gladiator'), # CQC rank - _('Elite') # Top rank + _('Helpless'), # LANG: CQC rank + _('Mostly Helpless'), # LANG: CQC rank + _('Amateur'), # LANG: CQC rank + _('Semi Professional'), # LANG: CQC rank + _('Professional'), # LANG: CQC rank + _('Champion'), # LANG: CQC rank + _('Hero'), # LANG: CQC rank + _('Gladiator'), # LANG: CQC rank + _('Elite') # LANG: Top rank ], # http://elite-dangerous.wikia.com/wiki/Federation#Ranks 'federation': [ - _('None'), # No rank - _('Recruit'), # Federation rank - _('Cadet'), # Federation rank - _('Midshipman'), # Federation rank - _('Petty Officer'), # Federation rank - _('Chief Petty Officer'), # Federation rank - _('Warrant Officer'), # Federation rank - _('Ensign'), # Federation rank - _('Lieutenant'), # Federation rank - _('Lieutenant Commander'), # Federation rank - _('Post Commander'), # Federation rank - _('Post Captain'), # Federation rank - _('Rear Admiral'), # Federation rank - _('Vice Admiral'), # Federation rank - _('Admiral') # Federation rank + _('None'), # LANG: No rank + _('Recruit'), # LANG: Federation rank + _('Cadet'), # LANG: Federation rank + _('Midshipman'), # LANG: Federation rank + _('Petty Officer'), # LANG: Federation rank + _('Chief Petty Officer'), # LANG: Federation rank + _('Warrant Officer'), # LANG: Federation rank + _('Ensign'), # LANG: Federation rank + _('Lieutenant'), # LANG: Federation rank + _('Lieutenant Commander'), # LANG: Federation rank + _('Post Commander'), # LANG: Federation rank + _('Post Captain'), # LANG: Federation rank + _('Rear Admiral'), # LANG: Federation rank + _('Vice Admiral'), # LANG: Federation rank + _('Admiral') # LANG: Federation rank ], # http://elite-dangerous.wikia.com/wiki/Empire#Ranks 'empire': [ - _('None'), # No rank - _('Outsider'), # Empire rank - _('Serf'), # Empire rank - _('Master'), # Empire rank - _('Squire'), # Empire rank - _('Knight'), # Empire rank - _('Lord'), # Empire rank - _('Baron'), # Empire rank - _('Viscount'), # Empire rank - _('Count'), # Empire rank - _('Earl'), # Empire rank - _('Marquis'), # Empire rank - _('Duke'), # Empire rank - _('Prince'), # Empire rank - _('King') # Empire rank + _('None'), # LANG: No rank + _('Outsider'), # LANG: Empire rank + _('Serf'), # LANG: Empire rank + _('Master'), # LANG: Empire rank + _('Squire'), # LANG: Empire rank + _('Knight'), # LANG: Empire rank + _('Lord'), # LANG: Empire rank + _('Baron'), # LANG: Empire rank + _('Viscount'), # LANG: Empire rank + _('Count'), # LANG: Empire rank + _('Earl'), # LANG: Empire rank + _('Marquis'), # LANG: Empire rank + _('Duke'), # LANG: Empire rank + _('Prince'), # LANG: Empire rank + _('King') # LANG: Empire rank ], # http://elite-dangerous.wikia.com/wiki/Ratings 'power': [ - _('None'), # No rank - _('Rating 1'), # Power rank - _('Rating 2'), # Power rank - _('Rating 3'), # Power rank - _('Rating 4'), # Power rank - _('Rating 5') # Power rank + _('None'), # LANG: No rank + _('Rating 1'), # LANG: Power rank + _('Rating 2'), # LANG: Power rank + _('Rating 3'), # LANG: Power rank + _('Rating 4'), # LANG: Power rank + _('Rating 5') # LANG: Power rank ], } @@ -169,7 +169,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]: res.append([title, names[rank] if rank < len(names) else f'Rank {rank}']) else: - res.append([title, _('None')]) # No rank + res.append([title, _('None')]) # LANG: No rank return res @@ -291,7 +291,9 @@ class StatsDialog(): return if not data.get('commander') or not data['commander'].get('name', '').strip(): - self.status['text'] = _("Who are you?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown commander + self.status['text'] = _("Who are you?!") elif ( not data.get('lastSystem') @@ -299,10 +301,14 @@ class StatsDialog(): or not data.get('lastStarport') or not data['lastStarport'].get('name', '').strip() ): - self.status['text'] = _("Where are you?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown location + self.status['text'] = _("Where are you?!") elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip(): - self.status['text'] = _("What are you flying?!") # Shouldn't happen + # Shouldn't happen + # LANG: Unknown ship + self.status['text'] = _("What are you flying?!") else: self.status['text'] = '' @@ -350,14 +356,14 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, thing, with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Status')) # Status dialog title + notebook.add(page, text=_('Status')) # LANG: Status dialog title page = self.addpage(notebook, [ - _('Ship'), # Status dialog subtitle + _('Ship'), # LANG: Status dialog subtitle '', - _('System'), # Main window - _('Station'), # Status dialog subtitle - _('Value'), # Status dialog subtitle - CR value of ship + _('System'), # LANG: Main window + _('Station'), # LANG: Status dialog subtitle + _('Value'), # LANG: Status dialog subtitle - CR value of ship ]) shiplist = ships(data) @@ -366,7 +372,7 @@ class StatsResults(tk.Toplevel): self.addpagerow(page, list(ship_data[1:-1]) + [self.credits(int(ship_data[-1]))], with_copy=True) ttk.Frame(page).grid(pady=5) # bottom spacer - notebook.add(page, text=_('Ships')) # Status dialog title + notebook.add(page, text=_('Ships')) # LANG: Status dialog title if platform != 'darwin': buttonframe = ttk.Frame(frame)