diff --git a/Scripts/unused_strings.py b/Scripts/unused_strings.py index eaf9771761..0e64c0b405 100755 --- a/Scripts/unused_strings.py +++ b/Scripts/unused_strings.py @@ -1,45 +1,100 @@ #!/usr/bin/env python3 """ -This script can be used to grep the source to tree to see which localized strings are in use. - -author: corbett -usage: ./unused_strings.py Localizable.strings source_dir -eg: ./unused_strings.py ../Signal/translations/en.lproj/Localizable.strings ../Signal/src +Find unused localization strings. """ + +import argparse import sys import os import re +import pathlib +from typing import Iterable -def file_match(fname, pat): - try: - f = open(fname, "rt") - except IOError: - return - - for i, line in enumerate(f): - if pat.search(line): - return True - f.close() - return False +project_root = pathlib.Path(__file__).parent.parent.resolve() +key_re = re.compile('^"([^"]+)" =') -def rgrep_match(dir_name, s_pat): - pat = re.compile(s_pat) - for dirpath, dirnames, filenames in os.walk(dir_name): - for fname in filenames: - fullname = os.path.join(dirpath, fname) - match=file_match(fullname, pat) - if match: - return match - return False +def project_path(from_root: str) -> pathlib.Path: + return project_root / from_root -if __name__ == '__main__': - strings_file = sys.argv[1] - src_dir_name = sys.argv[2] - for item in open(strings_file).readlines(): - grep_for = item.strip().split(' = ')[0].replace('"','') - if rgrep_match(src_dir_name, grep_for): - print(item.strip()) +def parse_args(): + parser = argparse.ArgumentParser(description="Find unused localization strings.") + parser.add_argument( + "--strings", + type=pathlib.Path, + default=project_path("Signal/translations/en.lproj/Localizable.strings"), + help="A Localizable.strings file", + ) + parser.add_argument( + "--src_dirs", + default=map( + project_path, + [ + "Signal/src", + "SignalMessaging", + "SignalNSE", + "SignalServiceKit", + "SignalShareExtension", + "SignalUI", + ], + ), + nargs="+", + type=pathlib.Path, + help="One or more source directories", + ) + return parser.parse_args() + +def get_all_keys(strings_file: pathlib.Path) -> Iterable[str]: + with open(strings_file, "r") as file: + for line in file: + match = key_re.match(line) + if match: + yield match.group(1) + + +def get_all_file_paths(src_dir: os.PathLike) -> Iterable[str]: + for root, _, files in os.walk(src_dir): + for name in files: + yield os.path.join(root, name) + + +def matching_substrings(file_path: str, substrings: Iterable[bytes]) -> Iterable[bytes]: + with open(file_path, "rb") as file: + contents = file.read() + return filter(lambda s: s in contents, substrings) + + +def get_unused_keys( + strings_file: pathlib.Path, src_dirs: Iterable[pathlib.Path] +) -> Iterable[str]: + all_keys = get_all_keys(strings_file) + all_keys_as_bytes = [key.encode() for key in all_keys] + + keys_not_seen = set(all_keys_as_bytes) + + for src_dir in src_dirs: + for file_path in get_all_file_paths(src_dir): + keys_seen = matching_substrings(file_path, keys_not_seen) + keys_not_seen -= set(keys_seen) + + return map(lambda k: k.decode(), keys_not_seen) + + +def main() -> None: + args = parse_args() + + unused_keys = get_unused_keys(args.strings, args.src_dirs) + sorted_unused_keys = sorted(unused_keys) + + for key in sorted_unused_keys: + print(key) + + if len(sorted_unused_keys) != 0: + sys.exit(1) + + +if __name__ == "__main__": + main()