2022-03-19 16:33:55 -08:00
|
|
|
|
import pathlib
|
2024-01-19 10:56:36 -06:00
|
|
|
|
from dataclasses import dataclass
|
2022-10-18 14:49:41 -08:00
|
|
|
|
from pathlib import Path
|
2022-03-19 16:33:55 -08:00
|
|
|
|
|
|
|
|
|
import dotenv
|
|
|
|
|
import requests
|
|
|
|
|
from jinja2 import Template
|
2024-03-10 12:58:52 -05:00
|
|
|
|
from pydantic import ConfigDict
|
2022-03-19 16:33:55 -08:00
|
|
|
|
from requests import Response
|
2022-10-18 14:49:41 -08:00
|
|
|
|
from utils import CodeDest, CodeKeys, inject_inline, log
|
2022-03-19 16:33:55 -08:00
|
|
|
|
|
2022-03-25 10:56:49 -08:00
|
|
|
|
from mealie.schema._mealie import MealieModel
|
|
|
|
|
|
2022-03-19 16:33:55 -08:00
|
|
|
|
BASE = pathlib.Path(__file__).parent.parent.parent
|
|
|
|
|
|
|
|
|
|
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY")
|
|
|
|
|
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
2024-01-19 10:56:36 -06:00
|
|
|
|
@dataclass
|
|
|
|
|
class LocaleData:
|
|
|
|
|
name: str
|
|
|
|
|
dir: str = "ltr"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOCALE_DATA: dict[str, LocaleData] = {
|
|
|
|
|
"en-US": LocaleData(name="American English"),
|
|
|
|
|
"en-GB": LocaleData(name="British English"),
|
|
|
|
|
"af-ZA": LocaleData(name="Afrikaans (Afrikaans)"),
|
|
|
|
|
"ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"),
|
|
|
|
|
"ca-ES": LocaleData(name="Català (Catalan)"),
|
|
|
|
|
"cs-CZ": LocaleData(name="Čeština (Czech)"),
|
|
|
|
|
"da-DK": LocaleData(name="Dansk (Danish)"),
|
|
|
|
|
"de-DE": LocaleData(name="Deutsch (German)"),
|
|
|
|
|
"el-GR": LocaleData(name="Ελληνικά (Greek)"),
|
|
|
|
|
"es-ES": LocaleData(name="Español (Spanish)"),
|
|
|
|
|
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
|
|
|
|
"fr-FR": LocaleData(name="Français (French)"),
|
2024-08-08 16:50:14 +02:00
|
|
|
|
"fr-BE": LocaleData(name="Belge (Belgian)"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"gl-ES": LocaleData(name="Galego (Galician)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"hu-HU": LocaleData(name="Magyar (Hungarian)"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"is-IS": LocaleData(name="Íslenska (Icelandic)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"it-IT": LocaleData(name="Italiano (Italian)"),
|
|
|
|
|
"ja-JP": LocaleData(name="日本語 (Japanese)"),
|
|
|
|
|
"ko-KR": LocaleData(name="한국어 (Korean)"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"lt-LT": LocaleData(name="Lietuvių (Lithuanian)"),
|
|
|
|
|
"lv-LV": LocaleData(name="Latviešu (Latvian)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"nl-NL": LocaleData(name="Nederlands (Dutch)"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"no-NO": LocaleData(name="Norsk (Norwegian)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"pl-PL": LocaleData(name="Polski (Polish)"),
|
|
|
|
|
"pt-BR": LocaleData(name="Português do Brasil (Brazilian Portuguese)"),
|
|
|
|
|
"pt-PT": LocaleData(name="Português (Portuguese)"),
|
|
|
|
|
"ro-RO": LocaleData(name="Română (Romanian)"),
|
|
|
|
|
"ru-RU": LocaleData(name="Pусский (Russian)"),
|
2024-04-19 05:42:50 -05:00
|
|
|
|
"sl-SI": LocaleData(name="Slovenščina (Slovenian)"),
|
2024-01-19 10:56:36 -06:00
|
|
|
|
"sr-SP": LocaleData(name="српски (Serbian)"),
|
|
|
|
|
"sv-SE": LocaleData(name="Svenska (Swedish)"),
|
|
|
|
|
"tr-TR": LocaleData(name="Türkçe (Turkish)"),
|
|
|
|
|
"uk-UA": LocaleData(name="Українська (Ukrainian)"),
|
|
|
|
|
"vi-VN": LocaleData(name="Tiếng Việt (Vietnamese)"),
|
|
|
|
|
"zh-CN": LocaleData(name="简体中文 (Chinese simplified)"),
|
|
|
|
|
"zh-TW": LocaleData(name="繁體中文 (Chinese traditional)"),
|
2022-03-19 16:33:55 -08:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-10 12:58:52 -05:00
|
|
|
|
LOCALE_TEMPLATE = """// This Code is auto generated by gen_ts_locales.py
|
2022-03-19 16:33:55 -08:00
|
|
|
|
export const LOCALES = [{% for locale in locales %}
|
|
|
|
|
{
|
|
|
|
|
name: "{{ locale.name }}",
|
|
|
|
|
value: "{{ locale.locale }}",
|
|
|
|
|
progress: {{ locale.progress }},
|
2024-01-19 10:56:36 -06:00
|
|
|
|
dir: "{{ locale.dir }}",
|
2022-03-19 16:33:55 -08:00
|
|
|
|
},{% endfor %}
|
|
|
|
|
]
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
2022-03-19 16:33:55 -08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2022-03-25 10:56:49 -08:00
|
|
|
|
class TargetLanguage(MealieModel):
|
2024-03-10 12:58:52 -05:00
|
|
|
|
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
|
|
|
|
2022-03-19 16:33:55 -08:00
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
locale: str
|
2024-01-20 10:34:57 -06:00
|
|
|
|
dir: str = "ltr"
|
2022-03-19 16:33:55 -08:00
|
|
|
|
threeLettersCode: str
|
|
|
|
|
twoLettersCode: str
|
|
|
|
|
progress: float = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CrowdinApi:
|
|
|
|
|
project_name = "Mealie"
|
|
|
|
|
project_id = "451976"
|
|
|
|
|
api_key = API_KEY
|
|
|
|
|
|
|
|
|
|
def __init__(self, api_key: str):
|
|
|
|
|
api_key = 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"]
|
|
|
|
|
|
|
|
|
|
models = [TargetLanguage(**t) for t in tls]
|
|
|
|
|
|
|
|
|
|
models.insert(
|
|
|
|
|
0,
|
|
|
|
|
TargetLanguage(
|
2024-01-19 10:56:36 -06:00
|
|
|
|
id="en-US",
|
|
|
|
|
name="English",
|
|
|
|
|
locale="en-US",
|
|
|
|
|
dir="ltr",
|
|
|
|
|
threeLettersCode="en",
|
|
|
|
|
twoLettersCode="en",
|
|
|
|
|
progress=100,
|
2022-03-19 16:33:55 -08:00
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
progress: list[dict] = self.get_progress()["data"]
|
|
|
|
|
|
|
|
|
|
for model in models:
|
2024-01-19 10:56:36 -06:00
|
|
|
|
if model.locale in LOCALE_DATA:
|
|
|
|
|
locale_data = LOCALE_DATA[model.locale]
|
|
|
|
|
model.name = locale_data.name
|
|
|
|
|
model.dir = locale_data.dir
|
2022-03-19 16:33:55 -08:00
|
|
|
|
|
|
|
|
|
for p in progress:
|
|
|
|
|
if p["data"]["languageId"] == model.id:
|
|
|
|
|
model.progress = p["data"]["translationProgress"]
|
|
|
|
|
|
|
|
|
|
models.sort(key=lambda x: x.locale, reverse=True)
|
|
|
|
|
return models
|
|
|
|
|
|
|
|
|
|
def get_progress(self) -> dict:
|
|
|
|
|
response = requests.get(
|
|
|
|
|
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
)
|
|
|
|
|
return response.json()
|
|
|
|
|
|
|
|
|
|
|
2022-08-02 10:41:44 -08:00
|
|
|
|
PROJECT_DIR = Path(__file__).parent.parent.parent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
|
|
|
|
|
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
|
2025-06-20 00:09:12 +07:00
|
|
|
|
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.ts"
|
|
|
|
|
i18n_config = PROJECT_DIR / "frontend" / "i18n.config.ts"
|
2024-03-10 12:58:52 -05:00
|
|
|
|
reg_valid = PROJECT_DIR / "mealie" / "schema" / "_mealie" / "validators.py"
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
This snippet walks the message and dat locales directories and generates the import information
|
2025-06-20 00:09:12 +07:00
|
|
|
|
for the nuxt.config.ts file and automatically injects it into the nuxt.config.ts file. Note that
|
2022-08-02 10:41:44 -08:00
|
|
|
|
the code generation ID is hardcoded into the script and required in the nuxt config.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def inject_nuxt_values():
|
|
|
|
|
all_date_locales = [
|
|
|
|
|
f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),' for match in datetime_dir.glob("*.json")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
all_langs = []
|
|
|
|
|
for match in locales_dir.glob("*.json"):
|
2025-06-20 00:09:12 +07:00
|
|
|
|
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}" }},'
|
2022-08-02 10:41:44 -08:00
|
|
|
|
all_langs.append(lang_string)
|
|
|
|
|
|
2022-10-18 14:49:41 -08:00
|
|
|
|
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
2022-08-02 10:41:44 -08:00
|
|
|
|
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
2025-06-20 00:09:12 +07:00
|
|
|
|
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
2022-03-19 16:33:55 -08:00
|
|
|
|
|
|
|
|
|
|
2024-03-10 12:58:52 -05:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2022-08-02 10:41:44 -08:00
|
|
|
|
def generate_locales_ts_file():
|
2022-03-19 16:33:55 -08:00
|
|
|
|
api = CrowdinApi("")
|
|
|
|
|
models = api.get_languages()
|
|
|
|
|
tmpl = Template(LOCALE_TEMPLATE)
|
|
|
|
|
rendered = tmpl.render(locales=models)
|
|
|
|
|
|
2022-10-18 14:49:41 -08:00
|
|
|
|
log.debug(f"generating locales ts file -> {CodeDest.use_locales}")
|
|
|
|
|
with open(CodeDest.use_locales, "w") as f:
|
2022-03-19 16:33:55 -08:00
|
|
|
|
f.write(rendered) # type:ignore
|
|
|
|
|
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
|
|
|
|
def main():
|
2022-10-18 14:49:41 -08:00
|
|
|
|
if API_KEY is None or API_KEY == "":
|
|
|
|
|
log.error("CROWDIN_API_KEY is not set")
|
|
|
|
|
return
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
2022-10-18 14:49:41 -08:00
|
|
|
|
generate_locales_ts_file()
|
2022-08-02 10:41:44 -08:00
|
|
|
|
inject_nuxt_values()
|
2024-03-10 12:58:52 -05:00
|
|
|
|
inject_registration_validation_values()
|
2022-08-02 10:41:44 -08:00
|
|
|
|
|
2022-03-19 16:33:55 -08:00
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|