2025-07-22 19:32:19 +03:00
|
|
|
# The MIT License (MIT)
|
|
|
|
|
|
|
|
# Copyright (c) 2025 ctih1
|
|
|
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
# THE SOFTWARE.
|
|
|
|
|
|
|
|
import os
|
|
|
|
from typing import Dict, List, Literal
|
|
|
|
import json
|
|
|
|
import sys
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
|
|
|
|
NOTICE = """
|
|
|
|
# This file was automatically created from localization JSON files.
|
|
|
|
# DO NOT EDIT THIS FILE DIRECTLY. If you want to edit a translation, please use the language's JSON file.
|
2025-07-23 10:19:08 +03:00
|
|
|
|
|
|
|
#fmt: off
|
2025-07-22 19:32:19 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("kaannos")
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
class LanguageCollector:
|
|
|
|
def __init__(self, language_dir: str) -> None:
|
|
|
|
self.path: str = language_dir
|
2025-07-23 10:19:08 +03:00
|
|
|
self.languages: Dict[str, Dict[str, str]] = {}
|
2025-07-22 19:32:19 +03:00
|
|
|
|
|
|
|
for file in os.listdir(self.path):
|
|
|
|
if not file.endswith(".json") or len(file) > 7:
|
|
|
|
logger.debug(f"Skipping {file}")
|
|
|
|
continue
|
|
|
|
|
|
|
|
locale: str = file.split(".json")[0]
|
|
|
|
logger.info(f"Discovered {file}")
|
|
|
|
with open(os.path.join(self.path, file), "r", encoding="UTF-8") as f:
|
2025-07-23 10:19:08 +03:00
|
|
|
keys: Dict[str, str] = json.load(f)
|
2025-07-22 19:32:19 +03:00
|
|
|
self.languages[locale] = keys
|
|
|
|
|
|
|
|
self.find_missing_keys()
|
|
|
|
|
|
|
|
def find_missing_keys(self) -> None:
|
|
|
|
primary_language_keys: Dict[str, str] = self.languages["en"]
|
|
|
|
|
|
|
|
for key in primary_language_keys:
|
|
|
|
for language in self.languages:
|
|
|
|
if key not in self.languages[language]:
|
|
|
|
logger.warning(f"Key {key} missing from {language}")
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
for language in self.languages:
|
|
|
|
for key in self.languages[language]:
|
|
|
|
if key not in primary_language_keys:
|
|
|
|
logger.warning(f"Leftover key {key} found from {language}")
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
class Script:
|
|
|
|
def __init__(self) -> None:
|
|
|
|
self.script: str = ""
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
def add_line(self, content, indent: int = 0, newline: bool = True) -> None:
|
2025-07-25 23:32:56 +03:00
|
|
|
tabs = "\t" * indent
|
|
|
|
newline_content = "\n" if newline else ""
|
|
|
|
|
|
|
|
self.script += f"{tabs}{content}{newline_content}"
|
2025-07-22 19:32:19 +03:00
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
def process_name(key: str) -> str:
|
2025-07-23 10:19:08 +03:00
|
|
|
return key.replace(" ", "_").replace(":", "").lower()
|
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
|
|
|
|
def find_args(string: str) -> List[str]:
|
|
|
|
variable_open: bool = False
|
|
|
|
temp_content: str = ""
|
|
|
|
|
|
|
|
variables: List[str] = []
|
|
|
|
for char in string:
|
|
|
|
if variable_open:
|
|
|
|
if char == "}":
|
|
|
|
variable_open = False
|
|
|
|
variables.append(temp_content)
|
|
|
|
temp_content = ""
|
|
|
|
continue
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
if char == "{":
|
|
|
|
raise SyntaxError("Variable already open!")
|
|
|
|
|
|
|
|
temp_content += char
|
|
|
|
|
|
|
|
else:
|
|
|
|
if char == "}":
|
|
|
|
raise SyntaxError("Trying to close a nonexistant variable")
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
if char == "{":
|
|
|
|
variable_open = True
|
2025-07-23 10:19:08 +03:00
|
|
|
|
2025-07-22 19:32:19 +03:00
|
|
|
return variables
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
|
|
|
|
def convert_args(
|
|
|
|
inp: str, vars: List[str], mode: Literal["brackets", "none"] = "brackets"
|
|
|
|
) -> str:
|
|
|
|
replacements = {".": "_", ",": "_"}
|
2025-07-22 19:32:19 +03:00
|
|
|
|
|
|
|
for var in vars:
|
|
|
|
cleaned_var = var
|
|
|
|
for key, val in replacements.items():
|
|
|
|
cleaned_var = cleaned_var.replace(key, val)
|
|
|
|
|
|
|
|
if mode == "none":
|
|
|
|
inp = inp.replace(f"{var}", f"{cleaned_var}")
|
|
|
|
else:
|
|
|
|
inp = inp.replace(f"{{{var}}}", f"{{{cleaned_var}}}")
|
|
|
|
|
|
|
|
return inp
|
|
|
|
|
|
|
|
|
|
|
|
class GenerateScript:
|
2025-07-23 10:19:08 +03:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
primary_lang: str,
|
|
|
|
language_data: Dict[str, Dict[str, str]],
|
|
|
|
use_typing: bool = True,
|
|
|
|
output_path: str = "out.py",
|
|
|
|
generate_comments: bool = True,
|
|
|
|
):
|
2025-07-22 19:32:19 +03:00
|
|
|
self.data = language_data
|
|
|
|
self.primary = primary_lang
|
|
|
|
self.script = Script()
|
|
|
|
self.uses_typing: bool = use_typing
|
|
|
|
self.output = output_path
|
|
|
|
self.generate_comments = generate_comments
|
|
|
|
|
|
|
|
def create(self):
|
2025-07-23 10:19:08 +03:00
|
|
|
# I really don't like this implementation but also it works
|
2025-07-22 19:32:19 +03:00
|
|
|
self.script.add_line(NOTICE)
|
|
|
|
if self.uses_typing:
|
|
|
|
self.script.add_line("from typing import Literal, List")
|
|
|
|
self.script.add_line(f"Language=Literal{list(self.data.keys())}")
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
f"languages: List[Language] = {list(self.data.keys())}"
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
self.script.add_line(f"default_lang: Language | str='{self.primary}'")
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
"def change_language(new_lang: Language | str) -> None: global default_lang; default_lang = new_lang"
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
else:
|
|
|
|
self.script.add_line(f"languages = {list(self.data.keys())}")
|
|
|
|
self.script.add_line(f"default_lang='{self.primary}'")
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
"def change_language(new_lang): global default_lang; default_lang = new_lang"
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
|
|
|
|
self.primary_data = self.data[self.primary]
|
|
|
|
|
|
|
|
for key in self.primary_data:
|
|
|
|
args = find_args(self.primary_data[key])
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
2025-07-25 23:32:56 +03:00
|
|
|
f"def {process_name(key)}({convert_args(','.join([*args, 'lang:str|None=None' if self.uses_typing else 'lang']), args, 'none')}):"
|
2025-07-23 10:19:08 +03:00
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
if self.generate_comments:
|
|
|
|
self.script.add_line('"""', 1)
|
|
|
|
self.script.add_line("### Locales", 1)
|
|
|
|
for language in self.data:
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
f"- {language.capitalize()}: **{self.data[language].get(key, self.primary_data[key])}**",
|
|
|
|
1,
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
self.script.add_line('"""', 1)
|
|
|
|
self.script.add_line("if not lang: lang=default_lang", 1)
|
|
|
|
for language in self.data:
|
|
|
|
formatted_map = "{"
|
|
|
|
for arg in args:
|
|
|
|
formatted_map += f'"{convert_args(arg, args, "none")}": {convert_args(arg, args, "none")},'
|
|
|
|
formatted_map = formatted_map[:-1] + "}"
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
f"""if lang == '{language}': return {convert_args(json.dumps(
|
2025-07-22 19:32:19 +03:00
|
|
|
self.data[language].get(key,self.primary_data[key]),
|
|
|
|
ensure_ascii=False
|
2025-07-23 10:19:08 +03:00
|
|
|
), args)}{f'.format_map({formatted_map})' if len(args) > 0 else ''}""",
|
|
|
|
1,
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
self.script.add_line(
|
|
|
|
"else: raise ValueError(f'Invalid language {lang}')", 1
|
|
|
|
)
|
2025-07-22 19:32:19 +03:00
|
|
|
with open(self.output, "w", encoding="UTF-8") as f:
|
|
|
|
f.write(self.script.script)
|
|
|
|
|
|
|
|
|
2025-07-23 10:19:08 +03:00
|
|
|
def build_result(
|
|
|
|
primary_lang: str,
|
|
|
|
locale_dir: str,
|
|
|
|
types: bool,
|
|
|
|
output_path: str,
|
|
|
|
generate_comments: bool = True,
|
|
|
|
):
|
2025-07-22 19:32:19 +03:00
|
|
|
start = time.time()
|
|
|
|
lc = LanguageCollector(locale_dir)
|
2025-07-23 10:19:08 +03:00
|
|
|
GenerateScript(
|
|
|
|
primary_lang, lc.languages, types, output_path, generate_comments
|
|
|
|
).create()
|
|
|
|
logger.info(f"Done in {time.time() - start}s")
|