diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml index 59e14a8c4d..851b5b1d6a 100644 --- a/.github/workflows/on_release.yml +++ b/.github/workflows/on_release.yml @@ -41,7 +41,7 @@ jobs: - name: Get release id: get_release - uses: bruceadams/get-release@v1.2.3 + uses: bruceadams/get-release@v1.3.1 - name: Upload built Assets to Release uses: actions/upload-release-asset@v1.0.2 diff --git a/frappe/__init__.py b/frappe/__init__.py index f8e4de34d1..ad07b11caf 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -89,7 +89,7 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str: _('Change') _('Change', context='Coins') """ - from frappe.translate import get_full_dict + from frappe.translate import get_all_translations from frappe.utils import is_html, strip_html_tags if not hasattr(local, "lang"): @@ -107,14 +107,15 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str: msg = as_unicode(msg).strip() translated_string = "" + + all_translations = get_all_translations(lang) if context: string_key = f"{msg}:{context}" - translated_string = get_full_dict(lang).get(string_key) + translated_string = all_translations.get(string_key) if not translated_string: - translated_string = get_full_dict(lang).get(msg) + translated_string = all_translations.get(msg) - # return lang_full_dict according to lang passed parameter return translated_string or non_translated_string @@ -222,7 +223,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: local.conf = _dict(get_site_config()) local.lang = local.conf.lang or "en" - local.lang_full_dict = None local.module_app = None local.app_modules = None diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py index ebed447b00..5602fa2c2d 100644 --- a/frappe/core/doctype/translation/test_translation.py +++ b/frappe/core/doctype/translation/test_translation.py @@ -3,6 +3,7 @@ import frappe from frappe import _ from frappe.tests.utils import FrappeTestCase +from frappe.translate import clear_cache class TestTranslation(FrappeTestCase): @@ -11,20 +12,17 @@ class TestTranslation(FrappeTestCase): def tearDown(self): frappe.local.lang = "en" - clear_translation_cache() + clear_cache() def test_doctype(self): translation_data = get_translation_data() for key, val in translation_data.items(): frappe.local.lang = key - clear_translation_cache() translation = create_translation(key, val) self.assertEqual(_(val[0]), val[1]) frappe.delete_doc("Translation", translation.name) - clear_translation_cache() - self.assertEqual(_(val[0]), val[0]) def test_parent_language(self): @@ -55,6 +53,10 @@ class TestTranslation(FrappeTestCase): clear_translation_cache() self.assertTrue(_(data[1][0]), data[1][1]) + def test_multi_language_translations(self): + source = "User" + self.assertNotEqual(_(source, lang="de"), _(source, lang="es")) + def test_html_content_data_translation(self): source = """ { - frappe.router.current_route = frappe.router.parse(); +watch(route, async () => { + frappe.router.current_route = await frappe.router.parse(); frappe.breadcrumbs.update(); frappe.recorder.route = route; }); diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index c453fcad6e..2005a46cfe 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -38,16 +38,17 @@ $("body").on("click", "a", function (e) { return false; }; - const href = e.currentTarget.getAttribute("href"); + const target_element = e.currentTarget; + const href = target_element.getAttribute("href"); + const is_on_same_host = target_element.hostname === window.location.hostname; // click handled, but not by href if ( - e.currentTarget.getAttribute("onclick") || // has a handler + target_element.getAttribute("onclick") || // has a handler e.ctrlKey || e.metaKey || // open in a new tab - href === "#" + href === "#" // hash is home ) { - // hash is home return; } @@ -57,20 +58,20 @@ $("body").on("click", "a", function (e) { if (href && href.startsWith("#")) { // target startswith "#", this is a v1 style route, so remake it. - return override(e.currentTarget.hash); + return override(target_element.hash); } - if (frappe.router.is_app_route(e.currentTarget.pathname)) { + if (is_on_same_host && frappe.router.is_app_route(target_element.pathname)) { // target has "/app, this is a v2 style route. - if (e.currentTarget.search) { + if (target_element.search) { frappe.route_options = {}; - let params = new URLSearchParams(e.currentTarget.search); + let params = new URLSearchParams(target_element.search); for (const [key, value] of params) { frappe.route_options[key] = value; } } - return override(e.currentTarget.pathname + e.currentTarget.hash); + return override(target_element.pathname + target_element.hash); } }); @@ -88,6 +89,7 @@ frappe.router = { "dashboard", "image", "inbox", + "map", ], list_views_route: { list: "List", @@ -100,6 +102,7 @@ frappe.router = { image: "Image", inbox: "Inbox", file: "Home", + map: "Map", }, layout_mapped: {}, diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index 82c2f4c567..e010d2abc0 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -135,7 +135,6 @@ class TestSearch(FrappeTestCase): def test_link_search_in_foreign_language(self): try: frappe.local.lang = "fr" - frappe.local.lang_full_dict = None # discard translation cache search_widget(doctype="DocType", txt="pay", page_length=20) output = frappe.response["values"] diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 8d30284cbd..647720a78f 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -108,7 +108,6 @@ def _restore_thread_locals(flags): frappe.local.conf = frappe._dict(frappe.get_site_config()) frappe.local.cache = {} frappe.local.lang = "en" - frappe.local.lang_full_dict = None frappe.local.preload_assets = {"style": [], "script": []} diff --git a/frappe/translate.py b/frappe/translate.py index c644636332..3f4a5f0c95 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -53,6 +53,12 @@ REPORT_TRANSLATE_PATTERN = re.compile('"([^:,^"]*):') CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}") +# Cache keys +MERGED_TRANSLATION_KEY = "merged_translations" +APP_TRANSLATION_KEY = "translations_from_apps" +USER_TRANSLATION_KEY = "lang_user_translations" + + def get_language(lang_list: list = None) -> str: """Set `frappe.local.lang` from HTTP headers at beginning of request @@ -215,7 +221,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]: def get_messages_for_boot(): """Return all message translations that are required on boot.""" - messages = get_full_dict(frappe.local.lang) + messages = get_all_translations(frappe.local.lang) messages.update(get_dict_from_hooks("boot", None)) return messages @@ -241,9 +247,9 @@ def make_dict_from_messages(messages, full_dict=None, load_user_translation=True out = {} if full_dict is None: if load_user_translation: - full_dict = get_full_dict(frappe.local.lang) + full_dict = get_all_translations(frappe.local.lang) else: - full_dict = load_lang(frappe.local.lang) + full_dict = get_translations_from_apps(frappe.local.lang) for m in messages: if m[1] in full_dict: @@ -266,31 +272,29 @@ def get_lang_js(fortype: str, name: str) -> str: return f"\n\n$.extend(frappe._messages, {json.dumps(get_dict(fortype, name))})" -def get_full_dict(lang: str) -> dict[str, str]: - """Load and return the entire translations dictionary for a language from :meth:`frape.cache` +def get_all_translations(lang: str) -> dict[str, str]: + """Load and return the entire translations dictionary for a language from apps + user translations. :param lang: Language Code, e.g. `hi` """ if not lang: return {} - # found in local, return! - if getattr(frappe.local, "lang_full_dict", None) is not None: - return frappe.local.lang_full_dict + def _merge_translations(): + all_translations = get_translations_from_apps(lang).copy() + try: + # get user specific translation data + user_translations = get_user_translations(lang) + all_translations.update(user_translations) + except Exception: + pass - frappe.local.lang_full_dict = load_lang(lang) + return all_translations - try: - # get user specific translation data - user_translations = get_user_translations(lang) - frappe.local.lang_full_dict.update(user_translations) - except Exception: - pass - - return frappe.local.lang_full_dict + return frappe.cache().hget(MERGED_TRANSLATION_KEY, lang, generator=_merge_translations) -def load_lang(lang, apps=None): +def get_translations_from_apps(lang, apps=None): """Combine all translations from `.csv` files in all `apps`. For derivative languages (es-GT), take translations from the base language (es) and then update translations from the child (es-GT)""" @@ -298,22 +302,20 @@ def load_lang(lang, apps=None): if lang == "en": return {} - out = frappe.cache().hget("lang_full_dict", lang, shared=True) - if not out: - out = {} + def _get_from_disk(): + translations = {} for app in apps or frappe.get_all_apps(True): path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv") - out.update(get_translation_dict_from_file(path, lang, app) or {}) - + translations.update(get_translation_dict_from_file(path, lang, app) or {}) if "-" in lang: parent = lang.split("-")[0] - parent_out = load_lang(parent) - parent_out.update(out) - out = parent_out + parent_translations = get_translations_from_apps(parent) + parent_translations.update(translations) + return parent_translations - frappe.cache().hset("lang_full_dict", lang, out, shared=True) + return translations - return out or {} + return frappe.cache().hget(APP_TRANSLATION_KEY, lang, shared=True, generator=_get_from_disk) def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, str]: @@ -342,23 +344,22 @@ def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, st def get_user_translations(lang): if not frappe.db: frappe.connect() - out = frappe.cache().hget("lang_user_translations", lang) - if out is None: - out = {} - user_translations = frappe.get_all( + + def _read_from_db(): + user_translations = {} + translations = frappe.get_all( "Translation", fields=["source_text", "translated_text", "context"], filters={"language": lang} ) - for translation in user_translations: - key = translation.source_text - value = translation.translated_text - if translation.context: - key += ":" + translation.context - out[key] = value + for t in translations: + key = t.source_text + value = t.translated_text + if t.context: + key += ":" + t.context + user_translations[key] = value + return user_translations - frappe.cache().hset("lang_user_translations", lang, out) - - return out + return frappe.cache().hget(USER_TRANSLATION_KEY, lang, generator=_read_from_db) def clear_cache(): @@ -368,9 +369,10 @@ def clear_cache(): # clear translations saved in boot cache cache.delete_key("bootinfo") - cache.delete_key("lang_full_dict", shared=True) cache.delete_key("translation_assets", shared=True) - cache.delete_key("lang_user_translations") + cache.delete_key(APP_TRANSLATION_KEY, shared=True) + cache.delete_key(USER_TRANSLATION_KEY) + cache.delete_key(MERGED_TRANSLATION_KEY) def get_messages_for_app(app, deduplicate=True): @@ -1050,7 +1052,7 @@ def get_untranslated(lang, untranslated_file, get_all=False, app="_ALL_APPS"): # replace \n with ||| so that internal linebreaks don't get split f.write((escape_newlines(m[1]) + os.linesep).encode("utf-8")) else: - full_dict = get_full_dict(lang) + full_dict = get_all_translations(lang) for m in messages: if not full_dict.get(m[1]): @@ -1073,7 +1075,7 @@ def update_translations(lang, untranslated_file, translated_file, app="_ALL_APPS :param untranslated_file: File path with the messages in English. :param translated_file: File path with messages in language to be updated.""" clear_cache() - full_dict = get_full_dict(lang) + full_dict = get_all_translations(lang) def restore_newlines(s): return ( @@ -1110,7 +1112,7 @@ def update_translations(lang, untranslated_file, translated_file, app="_ALL_APPS def import_translations(lang, path): """Import translations from file in standard format""" clear_cache() - full_dict = get_full_dict(lang) + full_dict = get_all_translations(lang) full_dict.update(get_translation_dict_from_file(path, lang, "import")) for app in frappe.get_all_apps(True): @@ -1140,7 +1142,9 @@ def write_translations_file(app, lang, full_dict=None, app_messages=None): tpath = frappe.get_pymodule_path(app, "translations") frappe.create_folder(tpath) - write_csv_file(os.path.join(tpath, lang + ".csv"), app_messages, full_dict or get_full_dict(lang)) + write_csv_file( + os.path.join(tpath, lang + ".csv"), app_messages, full_dict or get_all_translations(lang) + ) def send_translations(translation_dict): @@ -1302,3 +1306,8 @@ def get_translated_doctypes(): "Property Setter", {"property": "translated_doctype", "value": "1"}, pluck="doc_type" ) return unique(dts + custom_dts) + + +# Backward compatibility +get_full_dict = get_all_translations +load_lang = get_translations_from_apps diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index ed7110467b..78fdcb7925 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -2593,6 +2593,7 @@ Tree,Baum, Trigger Method,Trigger-Methode, Trigger Name,Name des Auslösers, "Trigger on valid methods like ""before_insert"", ""after_update"", etc (will depend on the DocType selected)","Trigger auf gültige Methoden wie "before_insert", "after_update" usw. (hängt von der DocType ausgewählt)", +Try a naming Series, Nummernkreis testen, Try to avoid repeated words and characters,"Versuchen Sie, wiederholte Wörter und Zeichen zu vermeiden", Try to use a longer keyboard pattern with more turns,"Versuchen Sie, eine längere Tastaturmuster mit mehr Windungen zu verwenden", Two Factor Authentication,Zwei-Faktor-Authentifizierung, diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index e0ae91fef7..03ba7c0880 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -98,7 +98,6 @@ def get_context(context): """Build context to render the `web_form.html` template""" context.in_edit_mode = False context.in_view_mode = False - self.set_web_form_module() if frappe.form_dict.is_list: context.template = "website/doctype/web_form/templates/web_list.html" @@ -284,13 +283,14 @@ def get_context(context): def add_custom_context_and_script(self, context): """Update context from module if standard and append script""" - if self.web_form_module: - new_context = self.web_form_module.get_context(context) + if self.is_standard: + web_form_module = get_web_form_module(self) + new_context = web_form_module.get_context(context) if new_context: context.update(new_context) - js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".js") + js_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".js") if os.path.exists(js_path): script = frappe.render_template(open(js_path).read(), context) @@ -300,9 +300,7 @@ def get_context(context): context.script = script - css_path = os.path.join( - os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".css" - ) + css_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".css") if os.path.exists(css_path): style = open(css_path).read() @@ -322,14 +320,6 @@ def get_context(context): return parents - def set_web_form_module(self): - """Get custom web form module if exists""" - self.web_form_module = self.get_web_form_module() - - def get_web_form_module(self): - if self.is_standard: - return get_doc_module(self.module, self.doctype, self.name) - def validate_mandatory(self, doc): """Validate mandatory web form fields""" missing = [] @@ -368,6 +358,11 @@ def get_context(context): return False +def get_web_form_module(doc): + if doc.is_standard: + return get_doc_module(doc.module, doc.doctype, doc.name) + + @frappe.whitelist(allow_guest=True) @rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) def accept(web_form, data, docname=None): diff --git a/frappe/website/doctype/website_theme/website_theme.js b/frappe/website/doctype/website_theme/website_theme.js index 5c0524f357..c22bee1bc8 100644 --- a/frappe/website/doctype/website_theme/website_theme.js +++ b/frappe/website/doctype/website_theme/website_theme.js @@ -19,6 +19,7 @@ frappe.ui.form.on("Website Theme", { } else { frm.enable_save(); } + frm.set_df_property("custom_scss", "max_lines", 45); }, set_default_theme_button_and_indicator(frm) { diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index ee4b33d854..0faa621f68 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -7,10 +7,10 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "bootstrap_theme_section", "theme", "module", "custom", - "bootstrap_theme_section", "google_font", "font_size", "font_properties", @@ -73,8 +73,8 @@ { "collapsible": 1, "fieldname": "custom_js_section", - "fieldtype": "Section Break", - "label": "Custom JS" + "fieldtype": "Tab Break", + "label": "Script" }, { "fieldname": "js", @@ -84,7 +84,7 @@ }, { "fieldname": "bootstrap_theme_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Theme Configuration" }, { @@ -123,7 +123,7 @@ }, { "fieldname": "stylesheet_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Stylesheet" }, { @@ -181,10 +181,11 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-18 17:43:39.804765", + "modified": "2022-10-25 22:15:53.601571", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -206,5 +207,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/www/list.py b/frappe/www/list.py index 06a2ea48aa..9b704d5d44 100644 --- a/frappe/www/list.py +++ b/frappe/www/list.py @@ -163,6 +163,7 @@ def prepare_filters(doctype, controller, kwargs): def get_list_context(context, doctype, web_form_name=None): from frappe.modules import load_doctype_module + from frappe.website.doctype.web_form.web_form import get_web_form_module list_context = context or frappe._dict() meta = frappe.get_meta(doctype) @@ -193,7 +194,7 @@ def get_list_context(context, doctype, web_form_name=None): # get context from web form module if web_form_name: web_form = frappe.get_doc("Web Form", web_form_name) - list_context = update_context_from_module(web_form.get_web_form_module(), list_context) + list_context = update_context_from_module(get_web_form_module(web_form), list_context) # get path from '/templates/' folder of the doctype if not meta.custom and not list_context.row_template: diff --git a/pyproject.toml b/pyproject.toml index 2f5d0cc2b3..e70a38215c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "Click~=7.1.2", "GitPython~=3.1.14", "Jinja2~=3.1.2", - "Pillow~=9.1.1", + "Pillow~=9.2.0", "PyJWT~=2.4.0", "PyMySQL~=1.0.2", "PyPDF2~=2.1.0",