Skip to content

Commit

Permalink
Simplify creation of translated f-strings in multilingual mode
Browse files Browse the repository at this point in the history
  • Loading branch information
janezd committed Jan 1, 2025
1 parent 8790e6e commit 97b2dfc
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 122 deletions.
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The available options are
`smart-quotes` (default: true)
: If set to `false`, strings in translated sources will have the same quotes as in the original source. Otherwise, if translation of a single-quoted includes a single quote, Trubar will output a double-quoted string and vice-versa. If translated message contains both types of quotes, they must be escaped with backslash.

This setting has not effect in multilingual setup.

`auto-prefix` (default: true)
: If set, Trubar will turn strings into f-strings if translation contains braces and adding an f- prefix makes it a syntactically valid string, *unless* the original string already included braces, in which case this may had been a pattern for `str.format`.

Expand Down
79 changes: 21 additions & 58 deletions trubar/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,61 +389,26 @@ def _f_string_languages(cls,
return add_f

@classmethod
def _get_quote(cls,
node: SomeString,
orig_str: str,
translation: str,
language_index: int,
prefix: str) -> str:
def _quoted_str(cls,
translation: str,
prefix: str,
orig_str: str,
language_index: int) -> str:
"""
Return a suitable quote for the given translation.
The method tries all quote types (starting with the original) and
returns the first one that compiles. If none compiles, raises a
TranslationError.
The method is used for f-strings.
Return a quoted string for the given translation.
"""
quotes = (node.quote, ) + (all_quotes if config.smart_quotes else ())
for quote in quotes:
try:
compiled = ast.parse(
f"{prefix}{quote}{translation}{quote}",
mode="eval")
except SyntaxError:
pass
else:
compiled = compiled.body
if isinstance(compiled, ast.JoinedStr) \
or isinstance(compiled, ast.Constant) \
and isinstance(compiled.value, str):
return quote

# No suitable quotes, raise an exception
hints = ""
if "f" in node.prefix:
hints += f"\n- String {orig_str} is an f-string"
else:
hints += (
"\n- Original string is not an f-string, but the translation \n"
"seems to be an f-string and auto-prefix option is set.")
if config.smart_quotes:
hints += \
"\n- I tried all quote types, even triple-quotes"
else:
hints += \
"\n- Try enabling smart quotes to allow changing the quote type"
if len(quote) != 3 and "\n" in translation:
hints += \
"\n- Check for any unescaped \\n's"

languages = list(config.languages.values())
language = languages[language_index].international_name
raise TranslationError(
f"Probable syntax error in translation to {language}.\n"
f"Original: {orig_str}\n"
f"Translation to {language}:\n {translation}\n"
"Some hints:" + hints)
quoted_str = prefix + repr(translation)
try:
ast.parse(quoted_str, mode="eval")
except SyntaxError:
languages = list(config.languages.values())
language = languages[language_index].international_name
raise TranslationError(
f"Probable syntax error in translation to {language}.\n"
f"Original: {orig_str}\n"
f"Translation:\n {translation}\n"
"Hints: this is being treated as an f-string; check for braces")
return quoted_str

def translate(
self,
Expand Down Expand Up @@ -476,9 +441,9 @@ def translate(
for lang_idx, (message, table) in \
enumerate(zip(messages, self.message_tables)):
prefix = fprefix if lang_idx in need_f else node.prefix
quote = self._get_quote(
node, orig_str, message, lang_idx, prefix)
table.append(f"{prefix}{quote}{message}{quote}")
table.append(
self._quoted_str(message, prefix, orig_str, lang_idx)
)
trans = f'_tr.e(_tr.c({idx}, {orig_str}))'
else:
for message, table in zip(messages, self.message_tables):
Expand All @@ -488,7 +453,6 @@ def translate(
trans = f"_tr.m[{idx}, {orig_str}]"
return cst.parse_expression(trans)


def collect(source: str,
existing: Optional[MsgDict] = None,
pattern: str = "",
Expand Down Expand Up @@ -660,7 +624,6 @@ def report(s, level):
with open(fname, "wt", encoding=config.encoding) as f:
json.dump(messages, f)


def _any_translations(translations: MsgDict):
return any(isinstance(value, str)
or isinstance(value, dict) and _any_translations(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["English", "English", "\"default\"", "\"some/directory\"", "f\"File {x}\"", "f'Not file {x + \".bak\"}'", "f\"\"\"{\"nonsense\"}\"\"\"", "'Import it, if you must.'", "Oranges"]
["English", "English", "'default'", "'some/directory'", "f'File {x}'", "f'Not file {x + \".bak\"}'", "f'{\"nonsense\"}'", "'Import it, if you must.'", "Oranges"]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["Foo", "Foolanguage", "\"befault\"", "f\"an {f} foo string\"", "f\"File {x}\"", "f'Ne datoteka {x + \".bak\"}'", "f\"\"\"{\"sense\"}\"\"\"", "f'''{x} +'\" {y}'''", "Flemons"]
["Foo", "Foolanguage", "'befault'", "f'an {f} foo string'", "f'File {x}'", "f'Ne datoteka {x + \".bak\"}'", "f'{\"sense\"}'", "f'{x} +\\'\" {y}'", "Flemons"]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["Sloven\u0161\u010dina", "Slovenian", "f\"\"\"An {f} st'r\"i'''ng\"\"\"", "\"some/directory\"", "f\"Datoteka {x}\"", "f'Ne datoteka {x + \".bak\"}'", "f\"\"\"{\"nesmisel\"}\"\"\"", "'Import it, if you must.'", "Pomaran\u010de"]
["Sloven\u0161\u010dina", "Slovenian", "f'An {f} st\\'r\"i\\'\\'\\'ng'", "'some/directory'", "f'Datoteka {x}'", "f'Ne datoteka {x + \".bak\"}'", "f'{\"nesmisel\"}'", "'Import it, if you must.'", "Pomaran\u010de"]
64 changes: 3 additions & 61 deletions trubar/tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@ class C:
""")
self.assertEqual(
message_tables,
[['msg1', 'msg2', 'msg3', 'foo', 'bar', 'f"baz{42}"', 'crux'],
['msg4', 'msg5', 'msg6', 'sea food', 'bar', 'f"baz{42}"', ''],
['msg7', 'msg8', 'msg8', 'foo', 'no-bar', 'f"bar(1)"', 'crux']]
[['msg1', 'msg2', 'msg3', 'foo', 'bar', "f'baz{42}'", 'crux'],
['msg4', 'msg5', 'msg6', 'sea food', 'bar', "f'baz{42}'", ''],
['msg7', 'msg8', 'msg8', 'foo', 'no-bar', "f'bar(1)'", 'crux']]
)

def test_f_string_languages(self):
Expand Down Expand Up @@ -413,52 +413,6 @@ def test_f_string_languages(self):
m(node, ["a string", "on'e", "tw'o{x}"]),
set())

def test_get_quote(self):
node = Mock()
m = StringTranslatorMultilingual._get_quote

node.prefix = ""
node.quote = '"'
self.assertEqual(
m(node, "'a string'", "a string", 2, ""), '"')

node.quote = "'''"
self.assertEqual(
m(node, "'a string'", "a string", 2, ""), "'''")

node.quote = "'"
self.assertEqual(
m(node, "'a string'", "a string", 2, ""), "'")

node.quote = "'"
self.assertEqual(
m(node, "'a string'", "tw'o{x}", 2, ""), '"')

node.quote = "'"
self.assertIn(
m(node, "'a str'ing'", "a str'i\"ng", 2, ""),
("'''", '"""'))

node.quote = "'"
self.assertEqual(
m(node, "'a str'''ing'", "s\"tr'''i'n\"g", 2, ""), '"""')

node.quote = "'"
self.assertRaises(
TranslationError,
m, node, "'a str'''ing'", "a \"\"\"s\"t\"r'''in'g", 2, "")

with patch("trubar.config.config.smart_quotes", False):
node.quote = "'"
self.assertRaises(
TranslationError,
m, node, "'a str'ing'", "a str'ing", 2, "")

node.quote = "'"
self.assertRaises(
TranslationError,
m, node, "'a str'''ing'", "a str'''ing", 2, "")

def test_auto_prefix(self):
# No f-strings, no problems
translation, tables = self._translate(
Expand Down Expand Up @@ -507,18 +461,6 @@ def test_smart_quotes_and_f(self):
tables,
[["f'foo'"], ["f\"don't\""], ["f'x\"y'"]])

with patch("trubar.config.config.smart_quotes", False):
# Mismatching quotes
self.assertRaises(
TranslationError,
self._translate, "print(f'foo')", [{"foo": "do{n}'t"}, {}])

# Original has an f-string, but quotes are OK
translation, tables = self._translate(
'print(f"foo")', [{"foo": "don't"}, {"foo": "x'y"}])
self.assertEqual(translation, 'print(_tr.e(_tr.c(0, f"foo")))')
self.assertEqual(tables, [['f"foo"'], ['f"don\'t"'], ['f"x\'y"']])

def test_syntax_error(self):
tree = cst.parse_module("print('foo')")
translator = yamlized(StringTranslator)({"foo": 'bar\nbaz'}, tree)
Expand Down

0 comments on commit 97b2dfc

Please sign in to comment.