diff --git a/frappe/boot.py b/frappe/boot.py index 1fe6f957d5..5ea9235b18 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -342,10 +342,10 @@ def get_user_pages_or_reports(parent, cache=False): def load_translations(bootinfo): - from frappe.translate import get_messages_for_boot + from frappe.translate import get_translation_version bootinfo["lang"] = frappe.lang - bootinfo["__messages"] = get_messages_for_boot() + 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..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 +from frappe.translate import MERGED_TRANSLATION_KEY, USER_TRANSLATION_KEY, change_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) + change_translation_version() 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..187e3fc956 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=${frappe.boot.translations_version}&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..682e0ca8d3 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -19,7 +19,8 @@ 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('"([^:,^"]*):') 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: @@ -132,6 +134,13 @@ def get_messages_for_boot(): return get_all_translations(frappe.local.lang) +@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.""" + 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. @@ -241,6 +250,21 @@ def clear_cache(): frappe.cache.delete_value( keys=["bootinfo", USER_TRANSLATION_KEY, MERGED_TRANSLATION_KEY], ) + 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 = frappe.generate_hash(length=8) + frappe.cache.set_value(TRANSLATION_VERSION_KEY, version) + return f"{version}_{get_build_version()}" + + +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): diff --git a/frappe/www/desk.html b/frappe/www/desk.html index ec4cb99974..b1aa7d7749 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=${frappe.boot.translations_version}&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(() => {}); +