|  |  |  | #!/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('<?xml version="1.0" encoding="utf-8"?>\n' +
 | 
					
						
							|  |  |  |              '<!DOCTYPE TS>\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()
 |