import os import argparse import re import glob from pathlib import Path # example usages: # python localizations.py generate # python localizations.py validate parser = argparse.ArgumentParser() parser.add_argument('command', type=str, help="Possible options: generate | validate") arguments = parser.parse_args() command = arguments.command if command not in ['generate', 'validate']: parser.print_help() exit() input_file_name = Path("Support/en.lproj/Localizable.strings") comment_file_name = Path("Support/qqq.lproj/Localizable.strings") enum_name = "LocalString" target_dir = "Support" template_file_name = "StringLocalExtension.swift_temp" class Generate: def __init__(self, input_file_name, template_file, enum_name, target_dir): self.enum_name = enum_name reader = Reader(input_file_name) vars = self.__variables(reader) with open(os.path.join(target_dir, template_file)) as template_file: template_content = template_file.read() output_path = os.path.join(target_dir, enum_name + ".swift") file = open(output_path, 'w') file.write("""// // !! DO NOT EDIT THIS FILE DIRECTLY !! // !! IT HAS BEEN AUTO-GENERATED !! // """) file.write(template_content) file.write(self.__code_for("\n\t".join(vars))) file.close() def __variables(self, reader): vars = list() for key, has_arguments in sorted(reader.keys()): if has_arguments: vars.append(self.__static_func_for(key)) else: vars.append(self.__static_let_for(key)) return vars def __code_for(self, variables): return """enum {} {{ {} }} """.format(self.enum_name, variables) def __static_let_for(self, key): return """static let {} = "{}".localized""".format(self.__get_var_name(key), key) def __static_func_for(self, key): return """static func {}(withArgs args: CVarArg...) -> String {{ "{}".localizedWithFormat(withArgs: args) }}""".format(self.__get_var_name(key), key) def __get_var_name(self, key): return re.sub('[^a-z0-9]', '_', key.lower()) class Reader: def __init__(self, input_file_name): self.input_file_name = input_file_name def keys(self): pattern = re.compile(r'"(?P.+)"\s{1,}=\s{1,}"(?P.+)"') with open(self.input_file_name) as input_file: for line in input_file: match = pattern.match(line) if match: groups = match.groupdict() key = groups.get('key') value = groups.get('value') has_arguments = "%@" in value yield key, has_arguments class Validate: def __init__(self, input_file_name, comment_file_name, enum_name, search_directory=os.getcwd()): reader = Reader(input_file_name) vars = list() for key, _ in reader.keys(): assert "." in key, "Invalid translation key: {}, it should contain at least one '.' (dot)".format(key) vars.append(key) vars = sorted(vars) matches = dict() counter = dict() for var in vars: swift_var = self.__get_var_name(var) counter[swift_var] = 0 for swift_file_name in glob.iglob(os.path.join(search_directory, '**/*.swift'), recursive=True): if Path(swift_file_name).name != "{}.swift".format(enum_name): with open(swift_file_name, 'r') as swift_file: content = swift_file.read() for var in vars: if var in content: if var in matches: matches[var].append(swift_file_name) else: matches[var] = [swift_file_name] swift_var = self.__get_var_name(var) if swift_var in content: counter[swift_var] += 1 assert len(matches.keys()) == 0, "localization strings cannot not be directly used in swift (use LocalString instead): {}".format(matches) unused_swift_vars = {k: v for k, v in counter.items() if v == 0 }.keys() assert len(unused_swift_vars) == 0, "unused localizations entries (delete them from localizations, and run: python localizations.py generate): {}".format(sorted(unused_swift_vars)) comment_keys = list() comment_reader = Reader(comment_file_name) for comment_key, _ in comment_reader.keys(): assert comment_key in vars, "extra qqq key found: {}".format(comment_key) comment_keys.append(comment_key) missing = sorted(set(vars).difference(comment_keys)) assert len(missing) == 0, "undocumented keys (please add them to qqq): {}".format(missing) def __get_var_name(self, key): return re.sub('[^a-z0-9]', '_', key.lower()) match command: case "generate": Generate(input_file_name, template_file_name, enum_name, target_dir) case "validate": Validate(input_file_name, comment_file_name, enum_name) case _: exit(-1)