diff --git a/dev/code-generation/_static.py b/dev/code-generation/_static.py index 1a0ee7ee6..d05fba101 100644 --- a/dev/code-generation/_static.py +++ b/dev/code-generation/_static.py @@ -16,6 +16,7 @@ class CodeTemplates: class CodeDest: interface = CWD / "generated" / "interface.js" pytest_routes = CWD / "generated" / "test_routes.py" + use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts" class CodeKeys: diff --git a/dev/code-generation/gen_frontend_types.py b/dev/code-generation/gen_frontend_types.py index 946db4153..4ece601c9 100644 --- a/dev/code-generation/gen_frontend_types.py +++ b/dev/code-generation/gen_frontend_types.py @@ -38,9 +38,9 @@ def generate_global_components_types() -> None: "layout": PROJECT_DIR / "frontend" / "components" / "Layout", } - def render_template(template: str, data: dict) -> None: - template = Template(template) - return template.render(**data) + def render_template(template: str, data: dict) -> str | None: + tmpl = Template(template) + return tmpl.render(**data) def build_data() -> dict: data = {} @@ -54,7 +54,9 @@ def generate_global_components_types() -> None: destination_file.write_text(text) text = render_template(template, build_data()) - write_template(text) + + if text: + write_template(text) # ============================================================ @@ -63,13 +65,13 @@ def generate_global_components_types() -> None: def generate_typescript_types() -> None: def path_to_module(path: Path): - path: str = str(path) + str_path: str = str(path) - path = path.removeprefix(str(PROJECT_DIR)) - path = path.removeprefix("/") - path = path.replace("/", ".") + str_path = str_path.removeprefix(str(PROJECT_DIR)) + str_path = str_path.removeprefix("/") + str_path = str_path.replace("/", ".") - return path + return str_path schema_path = PROJECT_DIR / "mealie" / "schema" types_dir = PROJECT_DIR / "frontend" / "types" / "api-types" @@ -94,12 +96,12 @@ def generate_typescript_types() -> None: try: path_as_module = path_to_module(module) - generate_typescript_defs(path_as_module, str(out_path), exclude=("CamelModel")) + generate_typescript_defs(path_as_module, str(out_path), exclude=("CamelModel")) # type: ignore except Exception as e: failed_modules.append(module) - print("\nModule Errors:", module, "-----------------") + print("\nModule Errors:", module, "-----------------") # noqa print(e) # noqa - print("Finished Module Errors:", module, "-----------------\n") + print("Finished Module Errors:", module, "-----------------\n") # noqa print("\n📁 Skipped Directories:") # noqa for skipped_dir in skipped_dirs: diff --git a/dev/code-generation/gen_locales.py b/dev/code-generation/gen_locales.py new file mode 100644 index 000000000..8ce5638a0 --- /dev/null +++ b/dev/code-generation/gen_locales.py @@ -0,0 +1,143 @@ +import pathlib + +import _static +import dotenv +import requests +from fastapi_camelcase import CamelModel +from jinja2 import Template +from requests import Response +from rich import print + +BASE = pathlib.Path(__file__).parent.parent.parent + +API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") + +NAMES = { + "en-US": "American English", + "en-GB": "British English", + "af-ZA": "Afrikaans (Afrikaans)", + "ar-SA": "العربية (Arabic)", + "ca-ES": "Català (Catalan)", + "cs-CZ": "Čeština (Czech)", + "da-DK": "Dansk (Danish)", + "de-DE": "Deutsch (German)", + "el-GR": "Ελληνικά (Greek)", + "es-ES": "Español (Spanish)", + "fi-FI": "Suomi (Finnish)", + "fr-FR": "Français (French)", + "he-IL": "עברית (Hebrew)", + "hu-HU": "Magyar (Hungarian)", + "it-IT": "Italiano (Italian)", + "ja-JP": "日本語 (Japanese)", + "ko-KR": "한국어 (Korean)", + "no-NO": "Norsk (Norwegian)", + "nl-NL": "Nederlands (Dutch)", + "pl-PL": "Polski (Polish)", + "pt-BR": "Português do Brasil (Brazilian Portuguese)", + "pt-PT": "Português (Portugese)", + "ro-RO": "Română (Romanian)", + "ru-RU": "Pусский (Russian)", + "sr-SP": "српски (Serbian)", + "sv-SE": "Svenska (Swedish)", + "tr-TR": "Türkçe (Turkish)", + "uk-UA": "Українська (Ukrainian)", + "vi-VN": "Tiếng Việt (Vietnamese)", + "zh-CN": "简体中文 (Chinese simplified)", + "zh-TW": "繁體中文 (Chinese traditional)", +} + +LOCALE_TEMPLATE = """// This Code is auto generated by gen_global_components.py +export const LOCALES = [{% for locale in locales %} + { + name: "{{ locale.name }}", + value: "{{ locale.locale }}", + progress: {{ locale.progress }}, + },{% endfor %} +] +""" + + +class TargetLanguage(CamelModel): + id: str + name: str + locale: str + 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( + id="en-US", name="English", locale="en-US", threeLettersCode="en", twoLettersCode="en", progress=100 + ), + ) + + progress: list[dict] = self.get_progress()["data"] + + for model in models: + if model.locale in NAMES: + model.name = NAMES[model.locale] + + 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() + + +def main(): + print("Starting...") # noqa + + if API_KEY is None: + print("CROWDIN_API_KEY is not set") # noqa + return + + api = CrowdinApi("") + models = api.get_languages() + tmpl = Template(LOCALE_TEMPLATE) + rendered = tmpl.render(locales=models) + + with open(_static.CodeDest.use_locales, "w") as f: + f.write(rendered) # type:ignore + + print("Finished...") # noqa + + +if __name__ == "__main__": + main() diff --git a/frontend/components/global/LanguageDialog.vue b/frontend/components/global/LanguageDialog.vue index 707984106..c1f11e392 100644 --- a/frontend/components/global/LanguageDialog.vue +++ b/frontend/components/global/LanguageDialog.vue @@ -1,33 +1,46 @@ - + diff --git a/frontend/composables/use-locales/available-locales.ts b/frontend/composables/use-locales/available-locales.ts new file mode 100644 index 000000000..ac50130c4 --- /dev/null +++ b/frontend/composables/use-locales/available-locales.ts @@ -0,0 +1,168 @@ +// This Code is auto generated by gen_global_components.py +export const LOCALES = [ + { + name: "繁體中文 (Chinese traditional)", + value: "zh-TW", + progress: 100, + }, + { + name: "简体中文 (Chinese simplified)", + value: "zh-CN", + progress: 100, + }, + { + name: "Tiếng Việt (Vietnamese)", + value: "vi-VN", + progress: 0, + }, + { + name: "Українська (Ukrainian)", + value: "uk-UA", + progress: 100, + }, + { + name: "Türkçe (Turkish)", + value: "tr-TR", + progress: 7, + }, + { + name: "Svenska (Swedish)", + value: "sv-SE", + progress: 100, + }, + { + name: "српски (Serbian)", + value: "sr-SP", + progress: 0, + }, + { + name: "Slovak", + value: "sk-SK", + progress: 100, + }, + { + name: "Pусский (Russian)", + value: "ru-RU", + progress: 100, + }, + { + name: "Română (Romanian)", + value: "ro-RO", + progress: 0, + }, + { + name: "Português (Portugese)", + value: "pt-PT", + progress: 15, + }, + { + name: "Português do Brasil (Brazilian Portuguese)", + value: "pt-BR", + progress: 64, + }, + { + name: "Polski (Polish)", + value: "pl-PL", + progress: 100, + }, + { + name: "Norsk (Norwegian)", + value: "no-NO", + progress: 100, + }, + { + name: "Nederlands (Dutch)", + value: "nl-NL", + progress: 100, + }, + { + name: "한국어 (Korean)", + value: "ko-KR", + progress: 0, + }, + { + name: "日本語 (Japanese)", + value: "ja-JP", + progress: 0, + }, + { + name: "Italiano (Italian)", + value: "it-IT", + progress: 99, + }, + { + name: "Magyar (Hungarian)", + value: "hu-HU", + progress: 100, + }, + { + name: "עברית (Hebrew)", + value: "he-IL", + progress: 0, + }, + { + name: "Français (French)", + value: "fr-FR", + progress: 100, + }, + { + name: "French, Canada", + value: "fr-CA", + progress: 100, + }, + { + name: "Suomi (Finnish)", + value: "fi-FI", + progress: 0, + }, + { + name: "Español (Spanish)", + value: "es-ES", + progress: 100, + }, + { + name: "American English", + value: "en-US", + progress: 100.0, + }, + { + name: "British English", + value: "en-GB", + progress: 100, + }, + { + name: "Ελληνικά (Greek)", + value: "el-GR", + progress: 100, + }, + { + name: "Deutsch (German)", + value: "de-DE", + progress: 100, + }, + { + name: "Dansk (Danish)", + value: "da-DK", + progress: 100, + }, + { + name: "Čeština (Czech)", + value: "cs-CZ", + progress: 0, + }, + { + name: "Català (Catalan)", + value: "ca-ES", + progress: 100, + }, + { + name: "العربية (Arabic)", + value: "ar-SA", + progress: 4, + }, + { + name: "Afrikaans (Afrikaans)", + value: "af-ZA", + progress: 0, + }, +]; diff --git a/frontend/composables/use-locales/index.ts b/frontend/composables/use-locales/index.ts new file mode 100644 index 000000000..b27286e0f --- /dev/null +++ b/frontend/composables/use-locales/index.ts @@ -0,0 +1 @@ +export { useLocales } from "./use-locales"; diff --git a/frontend/composables/use-locales/use-locales.ts b/frontend/composables/use-locales/use-locales.ts new file mode 100644 index 000000000..4767e56f6 --- /dev/null +++ b/frontend/composables/use-locales/use-locales.ts @@ -0,0 +1,23 @@ +import { computed, useContext } from "@nuxtjs/composition-api"; +import { LOCALES } from "./available-locales"; + +export const useLocales = () => { + const { i18n } = useContext(); + + const locale = computed({ + get() { + return i18n.locale; + }, + set(value) { + i18n.setLocale(value); + // Reload the page to update the language - not all strings are reactive + window.location.reload(); + }, + }); + + return { + locale, + locales: LOCALES, + i18n, + }; +}; diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index aeddde68e..2869e484a 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -497,7 +497,8 @@ "you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user" }, "language-dialog": { - "choose-language": "Choose language", + "translated": "translated", + "choose-language": "Choose Language", "select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.", "how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!", "read-the-docs": "Read the docs"