From 9ef5aa256ba1f6b3dbe93d59b7a64ba85b2ed432 Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Tue, 17 Mar 2026 19:08:53 +0530 Subject: [PATCH 1/6] perf: remove translations from boot --- frappe/boot.py | 3 --- frappe/public/js/frappe/desk.js | 4 +++- frappe/templates/base.html | 7 +++++++ frappe/translate.py | 7 +++++++ frappe/www/desk.html | 15 ++++++++++++++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 1fe6f957d5..c29a2118e5 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -342,10 +342,7 @@ def get_user_pages_or_reports(parent, cache=False): def load_translations(bootinfo): - from frappe.translate import get_messages_for_boot - bootinfo["lang"] = frappe.lang - bootinfo["__messages"] = get_messages_for_boot() def get_user_info(): diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 1408effc6a..e27968fac2 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -30,7 +30,9 @@ frappe.Application = class Application { this.startup(); } - startup() { + async startup() { + // Wait for translations to be loaded before rendering any UI + if (frappe._translations_loaded) await frappe._translations_loaded; frappe.realtime.init(); frappe.model.init(); diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 8dd3f12a47..56c114277a 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -98,6 +98,13 @@ frappe.boot = {{ frappe.utils.orjson_dumps(boot, default=frappe.json_handler) }} // for backward compatibility of some libs frappe.sys_defaults = frappe.boot.sysdefaults; + frappe._messages = {}; + frappe._translations_loaded = fetch( + `/api/method/frappe.translate.get_boot_translations?v=${window._version_number}&lang=${frappe.boot.lang}`, + {credentials: "same-origin"} + ).then(r => r.json()).then(data => { + frappe._messages = data.message || {}; + }).catch(() => {}); {{ include_script('frappe-web.bundle.js') }} {% endblock %} diff --git a/frappe/translate.py b/frappe/translate.py index 0a2d3cab50..50d5465405 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -132,6 +132,13 @@ def get_messages_for_boot(): return get_all_translations(frappe.local.lang) +@frappe.whitelist(allow_guest=True, methods=["GET"]) +def get_boot_translations(lang=None): + """Return all translations for the current user's language.""" + frappe.local.response_headers["Cache-Control"] = "private, max-age=31536000" + return get_all_translations(lang or frappe.local.lang) + + def get_all_translations(lang: str) -> dict[str, str]: """Load and return the entire translations dictionary for a language from apps + user translations. diff --git a/frappe/www/desk.html b/frappe/www/desk.html index ec4cb99974..b1b40c7258 100644 --- a/frappe/www/desk.html +++ b/frappe/www/desk.html @@ -52,9 +52,22 @@ if (!window.frappe) window.frappe = {}; frappe.boot = {{ frappe.utils.orjson_dumps(boot, default=frappe.json_handler) }}; - frappe._messages = frappe.boot["__messages"]; + frappe._messages = {}; frappe.csrf_token = "{{ csrf_token }}"; + frappe._translations_loaded = fetch( + `/api/method/frappe.translate.get_boot_translations?v=${window._version_number}&lang=${frappe.boot.lang}`, + { + credentials: "same-origin", + headers: { + "X-Frappe-CSRF-Token": frappe.csrf_token, + "Accept": "application/json" + } + } + ).then(r => r.json()).then(data => { + frappe._messages = data.message || {}; + }).catch(() => {}); + From a574651accb8cba4267e335b28210b14ff7ae014 Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Tue, 17 Mar 2026 20:13:27 +0530 Subject: [PATCH 2/6] refactor: add type hints to get_boot_translations --- frappe/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 50d5465405..5504b54990 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -133,7 +133,7 @@ def get_messages_for_boot(): @frappe.whitelist(allow_guest=True, methods=["GET"]) -def get_boot_translations(lang=None): +def get_boot_translations(lang: str | None = None) -> dict[str, str]: """Return all translations for the current user's language.""" frappe.local.response_headers["Cache-Control"] = "private, max-age=31536000" return get_all_translations(lang or frappe.local.lang) From 1a03e5af8dc08704834a2db5d5d24e3c7f6bdf78 Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Thu, 26 Mar 2026 12:27:39 +0530 Subject: [PATCH 3/6] fix: make translation version update for system and user translations both --- frappe/boot.py | 3 +++ .../core/doctype/translation/translation.py | 3 ++- frappe/templates/base.html | 2 +- frappe/translate.py | 22 ++++++++++++++++++- frappe/www/desk.html | 2 +- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index c29a2118e5..5ea9235b18 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -342,7 +342,10 @@ def get_user_pages_or_reports(parent, cache=False): def load_translations(bootinfo): + from frappe.translate import get_translation_version + bootinfo["lang"] = frappe.lang + bootinfo["translations_version"] = get_translation_version() def get_user_info(): diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py index b4a55e794e..51fabeaf71 100644 --- a/frappe/core/doctype/translation/translation.py +++ b/frappe/core/doctype/translation/translation.py @@ -5,7 +5,7 @@ import json import frappe from frappe.model.document import Document -from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY +from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, bump_translation_version from frappe.utils import is_html, strip_html_tags @@ -46,3 +46,4 @@ class Translation(Document): def clear_user_translation_cache(lang): frappe.cache.hdel(USER_TRANSLATION_KEY, lang) frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang) + bump_translation_version() diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 56c114277a..187e3fc956 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -100,7 +100,7 @@ frappe.sys_defaults = frappe.boot.sysdefaults; frappe._messages = {}; frappe._translations_loaded = fetch( - `/api/method/frappe.translate.get_boot_translations?v=${window._version_number}&lang=${frappe.boot.lang}`, + `/api/method/frappe.translate.get_boot_translations?v=${frappe.boot.translations_version}&lang=${frappe.boot.lang}`, {credentials: "same-origin"} ).then(r => r.json()).then(data => { frappe._messages = data.message || {}; diff --git a/frappe/translate.py b/frappe/translate.py index 5504b54990..12bd60f8c8 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -20,6 +20,7 @@ from csv import reader, writer import frappe from frappe.query_builder import DocType, Field from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags, unique +from frappe.utils.caching import http_cache REPORT_TRANSLATE_PATTERN = re.compile('"([^:,^"]*):') CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}") @@ -28,6 +29,7 @@ CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}") # Cache keys MERGED_TRANSLATION_KEY = "merged_translations" USER_TRANSLATION_KEY = "lang_user_translations" +TRANSLATION_VERSION_KEY = "translation_version" def get_language(lang_list: list | None = None) -> str: @@ -133,9 +135,9 @@ def get_messages_for_boot(): @frappe.whitelist(allow_guest=True, methods=["GET"]) +@http_cache(max_age=31536000) def get_boot_translations(lang: str | None = None) -> dict[str, str]: """Return all translations for the current user's language.""" - frappe.local.response_headers["Cache-Control"] = "private, max-age=31536000" return get_all_translations(lang or frappe.local.lang) @@ -248,6 +250,24 @@ def clear_cache(): frappe.cache.delete_value( keys=["bootinfo", USER_TRANSLATION_KEY, MERGED_TRANSLATION_KEY], ) + bump_translation_version() + + +def get_translation_version() -> str: + """Return the current translation version from cache.""" + version = frappe.cache.get_value(TRANSLATION_VERSION_KEY) + if version is None: + version = 1 + frappe.cache.set_value(TRANSLATION_VERSION_KEY, version) + return str(version) + + +def bump_translation_version(): + """Increment the translation version so browser caches are invalidated.""" + try: + frappe.cache.incrby(TRANSLATION_VERSION_KEY, 1) + except Exception: + frappe.cache.set_value(TRANSLATION_VERSION_KEY, 1) def get_messages_for_app(app, deduplicate=True): diff --git a/frappe/www/desk.html b/frappe/www/desk.html index b1b40c7258..b1aa7d7749 100644 --- a/frappe/www/desk.html +++ b/frappe/www/desk.html @@ -56,7 +56,7 @@ frappe.csrf_token = "{{ csrf_token }}"; frappe._translations_loaded = fetch( - `/api/method/frappe.translate.get_boot_translations?v=${window._version_number}&lang=${frappe.boot.lang}`, + `/api/method/frappe.translate.get_boot_translations?v=${frappe.boot.translations_version}&lang=${frappe.boot.lang}`, { credentials: "same-origin", headers: { From 81eb7d689285b537a0f8b534b76b0bc824a4f753 Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Thu, 26 Mar 2026 13:35:53 +0530 Subject: [PATCH 4/6] fix: increment translation version normally instead of incrby --- frappe/translate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/translate.py b/frappe/translate.py index 12bd60f8c8..d40a679f80 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -264,10 +264,8 @@ def get_translation_version() -> str: def bump_translation_version(): """Increment the translation version so browser caches are invalidated.""" - try: - frappe.cache.incrby(TRANSLATION_VERSION_KEY, 1) - except Exception: - frappe.cache.set_value(TRANSLATION_VERSION_KEY, 1) + current = frappe.cache.get_value(TRANSLATION_VERSION_KEY) or 0 + frappe.cache.set_value(TRANSLATION_VERSION_KEY, int(current) + 1) def get_messages_for_app(app, deduplicate=True): From 0b3e0f5c512b432957ffccea11085d2a2bc231ff Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Thu, 26 Mar 2026 15:41:43 +0530 Subject: [PATCH 5/6] refactor: use random string hash for translation version --- frappe/core/doctype/translation/translation.py | 4 ++-- frappe/translate.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py index 51fabeaf71..b7dc3b3e6e 100644 --- a/frappe/core/doctype/translation/translation.py +++ b/frappe/core/doctype/translation/translation.py @@ -5,7 +5,7 @@ import json import frappe from frappe.model.document import Document -from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, bump_translation_version +from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, change_translation_version from frappe.utils import is_html, strip_html_tags @@ -46,4 +46,4 @@ class Translation(Document): def clear_user_translation_cache(lang): frappe.cache.hdel(USER_TRANSLATION_KEY, lang) frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang) - bump_translation_version() + change_translation_version() diff --git a/frappe/translate.py b/frappe/translate.py index d40a679f80..8c2008b6f5 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -250,22 +250,21 @@ def clear_cache(): frappe.cache.delete_value( keys=["bootinfo", USER_TRANSLATION_KEY, MERGED_TRANSLATION_KEY], ) - bump_translation_version() + change_translation_version() def get_translation_version() -> str: """Return the current translation version from cache.""" version = frappe.cache.get_value(TRANSLATION_VERSION_KEY) if version is None: - version = 1 + version = frappe.generate_hash(length=8) frappe.cache.set_value(TRANSLATION_VERSION_KEY, version) - return str(version) + return version -def bump_translation_version(): - """Increment the translation version so browser caches are invalidated.""" - current = frappe.cache.get_value(TRANSLATION_VERSION_KEY) or 0 - frappe.cache.set_value(TRANSLATION_VERSION_KEY, int(current) + 1) +def change_translation_version(): + """Generate a new random translation version to invalidate browser caches.""" + frappe.cache.set_value(TRANSLATION_VERSION_KEY, frappe.generate_hash(length=8)) def get_messages_for_app(app, deduplicate=True): From aee33c5bbe936c7e5c8d04ff43a7f27ec9b2a0a4 Mon Sep 17 00:00:00 2001 From: Shrihari Mahabal Date: Fri, 27 Mar 2026 12:47:35 +0530 Subject: [PATCH 6/6] fix: add build version to translation version --- frappe/translate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/translate.py b/frappe/translate.py index 8c2008b6f5..682e0ca8d3 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -19,7 +19,7 @@ from csv import reader, writer import frappe from frappe.query_builder import DocType, Field -from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags, unique +from frappe.utils import cstr, get_bench_path, get_build_version, is_html, strip, strip_html_tags, unique from frappe.utils.caching import http_cache REPORT_TRANSLATE_PATTERN = re.compile('"([^:,^"]*):') @@ -259,7 +259,7 @@ def get_translation_version() -> str: if version is None: version = frappe.generate_hash(length=8) frappe.cache.set_value(TRANSLATION_VERSION_KEY, version) - return version + return f"{version}_{get_build_version()}" def change_translation_version():