@@ -36,7 +36,18 @@ class TestTranslate(unittest.TestCase): | |||||
def test_extract_message_from_file(self): | def test_extract_message_from_file(self): | ||||
data = frappe.translate.get_messages_from_file(translation_string_file) | data = frappe.translate.get_messages_from_file(translation_string_file) | ||||
self.assertListEqual(data, expected_output) | |||||
exp_filename = "apps/frappe/frappe/tests/translation_test_file.txt" | |||||
self.assertEqual(len(data), len(expected_output), | |||||
msg=f"Mismatched output:\nExpected: {expected_output}\nFound: {data}") | |||||
for extracted, expected in zip(data, expected_output): | |||||
ext_filename, ext_message, ext_context, ext_line = extracted | |||||
exp_message, exp_context, exp_line = expected | |||||
self.assertEqual(ext_filename, exp_filename) | |||||
self.assertEqual(ext_message, exp_message) | |||||
self.assertEqual(ext_context, exp_context) | |||||
self.assertEqual(ext_line, exp_line) | |||||
def test_translation_with_context(self): | def test_translation_with_context(self): | ||||
try: | try: | ||||
@@ -107,13 +118,16 @@ class TestTranslate(unittest.TestCase): | |||||
expected_output = [ | expected_output = [ | ||||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', None, 4), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 6), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 8), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 17), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 19), | |||||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 21) | |||||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||||
('Warning: Unable to find {0} in any table related to {1}', None, 4), | |||||
("You don't have any messages yet.", None, 6), | |||||
('Submit', 'Some DocType', 8), | |||||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||||
('Submit', 'Some DocType', 17), | |||||
("You don't have any messages yet.", None, 19), | |||||
("You don't have any messages yet.", None, 21), | |||||
("Long string that needs its own line because of black formatting.", None, 24), | |||||
("Long string with", "context", 28), | |||||
("Long string with", "context on newline", 32), | |||||
] | ] | ||||
@@ -18,4 +18,18 @@ _('Submit', context="Some DocType") | |||||
_("""You don't have any messages yet.""") | _("""You don't have any messages yet.""") | ||||
_('''You don't have any messages yet.''') | |||||
_('''You don't have any messages yet.''') | |||||
// allow newline in beginning | |||||
_( | |||||
"""Long string that needs its own line because of black formatting.""" | |||||
).format("blah") | |||||
_( | |||||
"Long string with", context="context" | |||||
).format("blah") | |||||
_( | |||||
"Long string with", | |||||
context="context on newline" | |||||
).format("blah") |
@@ -23,6 +23,35 @@ from frappe.utils import get_bench_path, is_html, strip, strip_html_tags | |||||
from frappe.query_builder import Field, DocType | from frappe.query_builder import Field, DocType | ||||
from pypika.terms import PseudoColumn | from pypika.terms import PseudoColumn | ||||
TRANSLATE_PATTERN = re.compile( | |||||
r"_\([\s\n]*" # starts with literal `_(`, ignore following whitespace/newlines | |||||
# BEGIN: message search | |||||
r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group | |||||
r"(?P<message>((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group | |||||
r"\1" # match exact string closing identifier | |||||
# END: message search | |||||
# BEGIN: python context search | |||||
r"([\s\n]*,[\s\n]*context\s*=\s*" # capture `context=` with ignoring whitespace | |||||
r"([\"'])" # start of context string identifier; 5th capture group | |||||
r"(?P<py_context>((?!\5).)*)" # capture context string till closing id is found | |||||
r"\5" # match context string closure | |||||
r")?" # match 0 or 1 context strings | |||||
# END: python context search | |||||
# BEGIN: JS context search | |||||
r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | [] | |||||
r"([\"'])" # start of context string; 11th capture group | |||||
r"(?P<js_context>((?!\11).)*)" # capture context string till closing id is found | |||||
r"\11" # match context string closure | |||||
r")*" | |||||
r")*" # match one or more context string | |||||
# END: JS context search | |||||
r"[\s\n]*\)" # Closing function call ignore leading whitespace/newlines | |||||
) | |||||
def get_language(lang_list: List = None) -> str: | def get_language(lang_list: List = None) -> str: | ||||
"""Set `frappe.local.lang` from HTTP headers at beginning of request | """Set `frappe.local.lang` from HTTP headers at beginning of request | ||||
@@ -651,9 +680,8 @@ def extract_messages_from_code(code): | |||||
frappe.clear_last_message() | frappe.clear_last_message() | ||||
messages = [] | messages = [] | ||||
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | |||||
for m in re.compile(pattern).finditer(code): | |||||
for m in TRANSLATE_PATTERN.finditer(code): | |||||
message = m.group('message') | message = m.group('message') | ||||
context = m.group('py_context') or m.group('js_context') | context = m.group('py_context') or m.group('js_context') | ||||
pos = m.start() | pos = m.start() | ||||