From 335c056a4b5a79f690d58146f56cacaf5ed99da7 Mon Sep 17 00:00:00 2001 From: Arc Date: Fri, 1 May 2026 13:05:47 +0100 Subject: [PATCH] feat: translation support for extensions --- lnbits/core/views/generic.py | 10 +++++--- lnbits/helpers.py | 48 +++++++++++++++++++++++++++++++++++- lnbits/templates/base.html | 4 ++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index e073a5c4f..9c024d04d 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -20,7 +20,7 @@ from lnbits.decorators import ( check_first_install, check_user_exists, ) -from lnbits.helpers import check_callback_url, template_renderer +from lnbits.helpers import check_callback_url, extension_id_from_path, template_renderer from lnbits.settings import settings from ..crud import get_user @@ -198,7 +198,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)] async def index( request: Request, user: User = Depends(check_user_exists) ) -> HTMLResponse: - return template_renderer().TemplateResponse( + return template_renderer( + extension_id=extension_id_from_path(request.url.path) + ).TemplateResponse( request, "base.html", { @@ -211,7 +213,9 @@ async def index( @generic_router.get("/node/public") @generic_router.get("/first_install", dependencies=[Depends(check_first_install)]) async def index_public(request: Request) -> HTMLResponse: - return template_renderer().TemplateResponse(request, "base.html", {"public": True}) + return template_renderer( + extension_id=extension_id_from_path(request.url.path) + ).TemplateResponse(request, "base.html", {"public": True}) @generic_router.get("/uuidv4/{hex_value}") diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 88a001b34..200bbf017 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -52,7 +52,45 @@ def static_url_for(static: str, path: str) -> str: return f"/{static}/{path}?v={settings.server_startup_time}" -def template_renderer(additional_folders: list | None = None) -> Jinja2Templates: +def extension_id_from_path(path: str) -> str | None: + parts = [part for part in path.split("/") if part] + if not parts: + return None + + if len(parts) >= 3 and parts[0] == "upgrades": + return parts[2] + + ext_id = parts[0] + ext_i18n_dir = Path( + settings.lnbits_extensions_path, "extensions", ext_id, "static", "i18n" + ) + if ext_i18n_dir.is_dir(): + return ext_id + + return None + + +def extension_i18n_urls(extension_id: str | None) -> list[str]: + if not extension_id: + return [] + + i18n_dir = Path( + settings.lnbits_extensions_path, "extensions", extension_id, "static", "i18n" + ) + if not i18n_dir.is_dir(): + return [] + + files = [file.name for file in i18n_dir.glob("*.js") if file.is_file()] + return [ + static_url_for(f"{extension_id}/static", f"i18n/{filename}") + for filename in sorted(files, key=lambda name: (name != "en.js", name)) + ] + + +def template_renderer( + additional_folders: list | None = None, + extension_id: str | None = None, +) -> Jinja2Templates: folders = [ "lnbits/templates", settings.extension_builder_working_dir_path.as_posix(), @@ -86,6 +124,14 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates t.env.globals["INCLUDED_CSS"] = vendor_files["css"] t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"] + if not extension_id and additional_folders: + for folder in additional_folders: + parts = Path(folder).parts + if parts and parts[-1] == "templates" and len(parts) >= 2: + extension_id = parts[-2] + break + t.env.globals["INCLUDED_EXTENSION_I18N"] = extension_i18n_urls(extension_id) + # backwards compatibility for extensions (tpos) t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination diff --git a/lnbits/templates/base.html b/lnbits/templates/base.html index c81271b2e..f98a38dd0 100644 --- a/lnbits/templates/base.html +++ b/lnbits/templates/base.html @@ -90,7 +90,9 @@ window.g.isPublicPage = false {% endif %} - {% endif %} + {% endif %} {% for url in INCLUDED_EXTENSION_I18N %} + + {% endfor %}