Files
mealie/dev/code-generation/gen_ts_locales.py
2026-02-12 19:07:23 -06:00

241 lines
7.6 KiB
Python

import json
import os
import pathlib
import re
from pathlib import Path
import dotenv
import requests
from jinja2 import Template
from pydantic import ConfigDict
from requests import Response
from utils import CodeDest, CodeKeys, inject_inline, log
from mealie.lang.locale_config import LOCALE_CONFIG, LocalePluralFoodHandling, LocaleTextDirection
from mealie.schema._mealie import MealieModel
BASE = pathlib.Path(__file__).parent.parent.parent
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
LOCALE_TEMPLATE = """// This Code is auto generated by gen_ts_locales.py
export const LOCALES = [{% for locale in locales %}
{
name: "{{ locale.name }}",
value: "{{ locale.locale }}",
progress: {{ locale.progress }},
dir: "{{ locale.dir }}",
pluralFoodHandling: "{{ locale.plural_food_handling }}",
},{% endfor %}
];
"""
class TargetLanguage(MealieModel):
model_config = ConfigDict(populate_by_name=True, extra="allow")
id: str
name: str
locale: str
dir: LocaleTextDirection = LocaleTextDirection.LTR
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
threeLettersCode: str
twoLettersCode: str
progress: int = 0
class CrowdinApi:
project_name = "Mealie"
project_id = "451976"
api_key = API_KEY
def __init__(self, api_key: str | None):
self.api_key = api_key or API_KEY
@property
def headers(self) -> dict:
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
def get_projects(self) -> Response:
return requests.get("https://api.crowdin.com/api/v2/projects", headers=self.headers)
def get_project(self) -> Response:
return requests.get(f"https://api.crowdin.com/api/v2/projects/{self.project_id}", headers=self.headers)
def get_languages(self) -> list[TargetLanguage]:
response = self.get_project()
tls = response.json()["data"]["targetLanguages"]
return [TargetLanguage(**t) for t in tls]
def get_progress(self) -> dict[str, int]:
response = requests.get(
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
headers=self.headers,
)
data = response.json()["data"]
return {p["data"]["languageId"]: p["translationProgress"] for p in data}
PROJECT_DIR = Path(__file__).parent.parent.parent
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.ts"
i18n_config = PROJECT_DIR / "frontend" / "i18n.config.ts"
reg_valid = PROJECT_DIR / "mealie" / "schema" / "_mealie" / "validators.py"
"""
This snippet walks the message and dat locales directories and generates the import information
for the nuxt.config.ts file and automatically injects it into the nuxt.config.ts file. Note that
the code generation ID is hardcoded into the script and required in the nuxt config.
"""
def inject_nuxt_values():
datetime_files = list(datetime_dir.glob("*.json"))
datetime_files.sort()
datetime_imports = []
datetime_object_entries = []
for match in datetime_files:
# Convert locale name to camelCase variable name (e.g., "en-US" -> "enUS")
var_name = match.stem.replace("-", "")
# Generate import statement
import_line = f'import * as {var_name} from "./lang/dateTimeFormats/{match.name}";'
datetime_imports.append(import_line)
# Generate object entry
object_entry = f' "{match.stem}": {var_name},'
datetime_object_entries.append(object_entry)
all_date_locales = datetime_imports + ["", "const datetimeFormats = {"] + datetime_object_entries + ["};"]
all_langs = []
for match in locales_dir.glob("*.json"):
match_data = LOCALE_CONFIG.get(match.stem)
match_dir = match_data.dir if match_data else LocaleTextDirection.LTR
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
all_langs.append(lang_string)
all_langs.sort()
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
def inject_registration_validation_values():
all_langs = []
for match in locales_dir.glob("*.json"):
lang_string = f'"{match.stem}",'
all_langs.append(lang_string)
# sort
all_langs.sort()
log.debug(f"injecting locales into user registration validation -> {reg_valid}")
inject_inline(reg_valid, CodeKeys.nuxt_local_messages, all_langs)
def _get_local_models() -> list[TargetLanguage]:
return [
TargetLanguage(
id=locale,
name=data.name,
locale=locale,
threeLettersCode=locale.split("-")[-1],
twoLettersCode=locale.split("-")[-1],
)
for locale, data in LOCALE_CONFIG.items()
if locale != "en-US" # Crowdin doesn't include this, so we manually inject it later
]
def _get_local_progress() -> dict[str, int]:
with open(CodeDest.use_locales) as f:
content = f.read()
# Extract the array content between [ and ]
match = re.search(r"export const LOCALES = (\[.*?\]);", content, re.DOTALL)
if not match:
raise ValueError("Could not find LOCALES array in file")
# Convert JS to JSON
array_content = match.group(1)
# Replace unquoted keys with quoted keys for valid JSON
# This converts: { name: "value" } to { "name": "value" }
json_str = re.sub(r"([,\{\s])([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'\1"\2":', array_content)
# Remove trailing commas before } and ]
json_str = re.sub(r",(\s*[}\]])", r"\1", json_str)
locales = json.loads(json_str)
return {locale["value"]: locale["progress"] for locale in locales}
def get_languages() -> list[TargetLanguage]:
if API_KEY:
api = CrowdinApi(None)
models = api.get_languages()
progress = api.get_progress()
else:
log.warning("CROWDIN_API_KEY is not set, using local lanugages instead")
log.warning("DOUBLE CHECK the output!!! Do not overwrite with bad local locale data!")
models = _get_local_models()
progress = _get_local_progress()
models.insert(
0,
TargetLanguage(
id="en-US",
name="English",
locale="en-US",
dir=LocaleTextDirection.LTR,
plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT,
threeLettersCode="en",
twoLettersCode="en",
progress=100,
),
)
for model in models:
if model.locale in LOCALE_CONFIG:
locale_data = LOCALE_CONFIG[model.locale]
model.name = locale_data.name
model.dir = locale_data.dir
model.plural_food_handling = locale_data.plural_food_handling
model.progress = progress.get(model.id, model.progress)
models.sort(key=lambda x: x.locale, reverse=True)
return models
def generate_locales_ts_file():
models = get_languages()
tmpl = Template(LOCALE_TEMPLATE)
rendered = tmpl.render(locales=models)
log.debug(f"generating locales ts file -> {CodeDest.use_locales}")
with open(CodeDest.use_locales, "w") as f:
f.write(rendered) # type:ignore
def main():
generate_locales_ts_file()
inject_nuxt_values()
inject_registration_validation_values()
if __name__ == "__main__":
main()