#!/usr/bin/env python3 import argparse import json import os import pathlib import xml.etree.ElementTree as ET from typing import cast import requests TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json" OPENAI_MODEL = "gpt-4" OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \ "The following sentence or word is in the GUI of a software called openpilot, translate it accordingly." def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]: files = {} with open(TRANSLATIONS_LANGUAGES) as fp: language_dict = json.load(fp) for filename in language_dict.values(): path = TRANSLATIONS_DIR / f"{filename}.ts" language = path.stem.split("main_")[1] if languages is None or language in languages: files[language] = path return files def translate_phrase(text: str, language: str) -> str: response = requests.post( "https://api.openai.com/v1/chat/completions", json={ "model": OPENAI_MODEL, "messages": [ { "role": "system", "content": OPENAI_PROMPT.format(language=language), }, { "role": "user", "content": text, }, ], "temperature": 0.8, "max_tokens": 1024, "top_p": 1, }, headers={ "Authorization": f"Bearer {OPENAI_API_KEY}", "Content-Type": "application/json", }, ) if 400 <= response.status_code < 600: raise requests.HTTPError(f'Error {response.status_code}: {response.json()}', response=response) data = response.json() return cast(str, data["choices"][0]["message"]["content"]) def translate_file(path: pathlib.Path, language: str, all_: bool) -> None: tree = ET.parse(path) root = tree.getroot() for context in root.findall("./context"): name = context.find("name") if name is None: raise ValueError("name not found") print(f"Context: {name.text}") for message in context.findall("./message"): source = message.find("source") translation = message.find("translation") if source is None or translation is None: raise ValueError("source or translation not found") if not all_ and translation.attrib.get("type") != "unfinished": continue llm_translation = translate_phrase(cast(str, source.text), language) print(f"Source: {source.text}\n" + f"Current translation: {translation.text}\n" + f"LLM translation: {llm_translation}") translation.text = llm_translation with path.open("w", encoding="utf-8") as fp: fp.write('\n' + '\n' + ET.tostring(root, encoding="utf-8").decode()) def main(): arg_parser = argparse.ArgumentParser("Auto translate") group = arg_parser.add_mutually_exclusive_group(required=True) group.add_argument("-a", "--all-files", action="store_true", help="Translate all files") group.add_argument("-f", "--file", nargs="+", help="Translate the selected files. (Example: -f fr de)") arg_parser.add_argument("-t", "--all-translations", action="store_true", default=False, help="Translate all sections. (Default: only unfinished)") args = arg_parser.parse_args() if OPENAI_API_KEY is None: print("OpenAI API key is missing. (Hint: use `export OPENAI_API_KEY=YOUR-KEY` before you run the script).\n" + "If you don't have one go to: https://beta.openai.com/account/api-keys.") exit(1) files = get_language_files(None if args.all_files else args.file) if args.file: missing_files = set(args.file) - set(files) if len(missing_files): print(f"No language files found: {missing_files}") exit(1) print(f"Translation mode: {'all' if args.all_translations else 'only unfinished'}. Files: {list(files)}") for lang, path in files.items(): print(f"Translate {lang} ({path})") translate_file(path, lang, args.all_translations) if __name__ == "__main__": main()