diff --git a/frappe/__init__.py b/frappe/__init__.py index 1568aa9f20..84dd5c2675 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -118,6 +118,12 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str: return translated_string or non_translated_string +def _lt(msg: str, lang: str | None = None, context: str | None = None): + from frappe.translate import LazyTranslate + + return LazyTranslate(msg, lang, context) + + def as_unicode(text, encoding: str = "utf-8") -> str: """Convert to unicode if required.""" if isinstance(text, str): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index d63579dff6..2dad3df4fd 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -3,7 +3,7 @@ # model __init__.py import frappe -from frappe import _ +from frappe import _, _lt data_fieldtypes = ( "Currency", @@ -134,22 +134,22 @@ log_types = ( ) std_fields = [ - {"fieldname": "name", "fieldtype": "Link", "label": _("ID")}, - {"fieldname": "owner", "fieldtype": "Link", "label": _("Created By"), "options": "User"}, - {"fieldname": "idx", "fieldtype": "Int", "label": _("Index")}, - {"fieldname": "creation", "fieldtype": "Datetime", "label": _("Created On")}, - {"fieldname": "modified", "fieldtype": "Datetime", "label": _("Last Updated On")}, + {"fieldname": "name", "fieldtype": "Link", "label": _lt("ID")}, + {"fieldname": "owner", "fieldtype": "Link", "label": _lt("Created By"), "options": "User"}, + {"fieldname": "idx", "fieldtype": "Int", "label": _lt("Index")}, + {"fieldname": "creation", "fieldtype": "Datetime", "label": _lt("Created On")}, + {"fieldname": "modified", "fieldtype": "Datetime", "label": _lt("Last Updated On")}, { "fieldname": "modified_by", "fieldtype": "Link", - "label": _("Last Updated By"), + "label": _lt("Last Updated By"), "options": "User", }, - {"fieldname": "_user_tags", "fieldtype": "Data", "label": _("Tags")}, - {"fieldname": "_liked_by", "fieldtype": "Data", "label": _("Liked By")}, - {"fieldname": "_comments", "fieldtype": "Text", "label": _("Comments")}, - {"fieldname": "_assign", "fieldtype": "Text", "label": _("Assigned To")}, - {"fieldname": "docstatus", "fieldtype": "Int", "label": _("Document Status")}, + {"fieldname": "_user_tags", "fieldtype": "Data", "label": _lt("Tags")}, + {"fieldname": "_liked_by", "fieldtype": "Data", "label": _lt("Liked By")}, + {"fieldname": "_comments", "fieldtype": "Text", "label": _lt("Comments")}, + {"fieldname": "_assign", "fieldtype": "Text", "label": _lt("Assigned To")}, + {"fieldname": "docstatus", "fieldtype": "Int", "label": _lt("Document Status")}, ] diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 232e2fcd81..7c4a1cf15e 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -22,7 +22,7 @@ from datetime import datetime import click import frappe -from frappe import _ +from frappe import _, _lt from frappe.model import ( child_table_fields, data_fieldtypes, @@ -42,17 +42,17 @@ from frappe.modules import load_doctype_module from frappe.utils import cast, cint, cstr DEFAULT_FIELD_LABELS = { - "name": lambda: _("ID"), - "creation": lambda: _("Created On"), - "docstatus": lambda: _("Document Status"), - "idx": lambda: _("Index"), - "modified": lambda: _("Last Updated On"), - "modified_by": lambda: _("Last Updated By"), - "owner": lambda: _("Created By"), - "_user_tags": lambda: _("Tags"), - "_liked_by": lambda: _("Liked By"), - "_comments": lambda: _("Comments"), - "_assign": lambda: _("Assigned To"), + "name": _lt("ID"), + "creation": _lt("Created On"), + "docstatus": _lt("Document Status"), + "idx": _lt("Index"), + "modified": _lt("Last Updated On"), + "modified_by": _lt("Last Updated By"), + "owner": _lt("Created By"), + "_user_tags": _lt("Tags"), + "_liked_by": _lt("Liked By"), + "_comments": _lt("Comments"), + "_assign": _lt("Assigned To"), } @@ -249,7 +249,7 @@ class Meta(Document): return df.get("label") if fieldname in DEFAULT_FIELD_LABELS: - return DEFAULT_FIELD_LABELS[fieldname]() + return str(DEFAULT_FIELD_LABELS[fieldname]) return "No Label" diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index ff776528f2..a4a27c559f 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -7,7 +7,7 @@ from unittest.mock import patch import frappe import frappe.translate -from frappe import _ +from frappe import _, _lt from frappe.gettext.extractors.javascript import extract_javascript from frappe.tests.utils import FrappeTestCase from frappe.translate import ( @@ -32,6 +32,9 @@ first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices( ) +_lazy_translations = _lt("Communication") + + class TestTranslate(FrappeTestCase): guest_sessions_required = [ "test_guest_request_language_resolution_with_cookie", @@ -46,6 +49,7 @@ class TestTranslate(FrappeTestCase): frappe.form_dict.pop("_lang", None) if self._testMethodName in self.guest_sessions_required: frappe.set_user("Administrator") + frappe.local.lang = "en" def test_clear_cache(self): _("Trigger caching") @@ -79,7 +83,6 @@ class TestTranslate(FrappeTestCase): self.assertEqual(ext_line, exp_line) def test_read_language_variant(self): - frappe.local.lang = "en" self.assertEqual(_("Mobile No"), "Mobile No") try: frappe.local.lang = "pt-BR" @@ -91,12 +94,24 @@ class TestTranslate(FrappeTestCase): self.assertEqual(_("Mobile No"), "Mobile No") def test_translation_with_context(self): - try: - frappe.local.lang = "fr" - self.assertEqual(_("Change"), "Changement") - self.assertEqual(_("Change", context="Coins"), "la monnaie") - finally: - frappe.local.lang = "en" + frappe.local.lang = "fr" + self.assertEqual(_("Change"), "Changement") + self.assertEqual(_("Change", context="Coins"), "la monnaie") + + def test_lazy_translations(self): + frappe.local.lang = "de" + eager_translation = _("Communication") + self.assertEqual(str(_lazy_translations), eager_translation) + self.assertRaises(NotImplementedError, lambda: _lazy_translations == "blah") + + # auto casts when added or radded + self.assertEqual(_lazy_translations + "A", eager_translation + "A") + x = _lazy_translations + x += "A" + self.assertEqual(x, eager_translation + "A") + + # f string usually auto-casts + self.assertEqual(f"{_lazy_translations}", eager_translation) def test_request_language_resolution_with_form_dict(self): """Test for frappe.translate.get_language diff --git a/frappe/translate.py b/frappe/translate.py index 643f3bf3e5..1cf656c959 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -1105,6 +1105,44 @@ def print_language(language: str): frappe.local.jenv = _jenv +@functools.total_ordering +class LazyTranslate: + __slots__ = ("msg", "lang", "context") + + def __init__(self, msg: str, lang: str | None = None, context: str | None = None) -> None: + self.msg = msg + self.lang = lang + self.context = context + + @property + def value(self) -> str: + return frappe._(str(self.msg), self.lang, self.context) + + def __str__(self): + return self.value + + def __add__(self, other): + if isinstance(other, (str, LazyTranslate)): + return self.value + str(other) + raise NotImplementedError + + def __radd__(self, other): + if isinstance(other, (str, LazyTranslate)): + return str(other) + self.value + return NotImplementedError + + def __repr__(self) -> str: + return f"'{self.value}'" + + # NOTE: it's required to override these methods and raise error as default behaviour will + # return `False` in all cases. + def __eq__(self, other): + raise NotImplementedError + + def __lt__(self, other): + raise NotImplementedError + + # Backward compatibility get_full_dict = get_all_translations load_lang = get_translations_from_apps diff --git a/frappe/utils/password_strength.py b/frappe/utils/password_strength.py index 253421a6d4..70ca375938 100644 --- a/frappe/utils/password_strength.py +++ b/frappe/utils/password_strength.py @@ -7,7 +7,7 @@ from zxcvbn import zxcvbn from zxcvbn.scoring import ALL_UPPER, START_UPPER import frappe -from frappe import _ +from frappe import _, _lt if TYPE_CHECKING: from collections.abc import Iterable @@ -41,8 +41,8 @@ def test_password_strength(password: str, user_inputs: "Iterable[object]" = None default_feedback: "PasswordStrengthFeedback" = { "warning": "", "suggestions": [ - _("Use a few words, avoid common phrases."), - _("No need for symbols, digits, or uppercase letters."), + _lt("Use a few words, avoid common phrases."), + _lt("No need for symbols, digits, or uppercase letters."), ], }