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 @@
-
-
- {{ $t('language-dialog.select-description') }}
-
-
-
- {{ $t("language-dialog.read-the-docs") }}
-
-
-
-
+
+
+ {{ $t("language-dialog.select-description") }}
+
+
+
+
+ {{ item.progress }}% {{ $tc("language-dialog.translated") }}
+
+
+
+
+
+ {{
+ $t("language-dialog.read-the-docs")
+ }}
+
+
+
+
-
+
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"