From 61002ea45309851a61de2650b599baee3f2df1f9 Mon Sep 17 00:00:00 2001 From: Christopher Krah <48338417+chrikrah@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:31:06 +0100 Subject: [PATCH] pofile: Unescape the C escapes GNU gettext writes GNU xgettext and msgcat write BEL, BS, FF and VT inside PO strings as \a, \b, \f and \v. read_po only decoded \n, \r, \t, \\ and \", so these four stayed as a backslash and a letter, and a catalog compiled by Babel looked the message up under the wrong msgid. Signed-off-by: Christopher Krah <48338417+chrikrah@users.noreply.github.com> --- babel/messages/pofile.py | 13 ++++--------- tests/messages/test_pofile.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index e3b99e4b6..63401f10d 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -26,7 +26,8 @@ from _typeshed import SupportsWrite -_unescape_re = re.compile(r'\\([\\trn"])') +_unescape_re = re.compile(r'\\([\\abfnrtv"])') +_unescape_map = {'a': '\a', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v'} def unescape(string: str) -> str: @@ -42,14 +43,8 @@ def unescape(string: str) -> str: def replace_escapes(match): m = match.group(1) - if m == 'n': - return '\n' - elif m == 't': - return '\t' - elif m == 'r': - return '\r' - # m is \ or " - return m + # m is a letter of a C escape, or \ or " + return _unescape_map.get(m, m) if "\\" not in string: # Fast path: there's nothing to unescape return string[1:-1] diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 1b753eac8..14db8182f 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -87,6 +87,20 @@ def test_unescape_of_quoted_newline(): assert pofile.unescape(r'"\\n"') == '\\n' +def test_unescape_of_c_escapes(): + # GNU xgettext and msgcat write these control characters as C escapes + assert pofile.unescape(r'"\a\b\f\n\r\t\v"') == '\a\b\f\n\r\t\v' + + +def test_read_po_unescapes_vertical_tab(): + buf = StringIO(r''' +msgid "col1\vcol2" +msgstr "Spalte1\vSpalte2" +''') + catalog = pofile.read_po(buf) + assert catalog.get('col1\vcol2').string == 'Spalte1\vSpalte2' + + def test_denormalize_on_msgstr_without_empty_first_line(): # handle irregular multi-line msgstr (no "" as first line) # gracefully (#171)