Skip to content

Bug 1434998 - Allow for REPLACE values to be Patterns or PatternElements #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 42 additions & 38 deletions fluent/migrate/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"""

from __future__ import unicode_literals
import itertools

import fluent.syntax.ast as FTL
from .errors import NotSupportedError
Expand Down Expand Up @@ -128,11 +129,11 @@ def __call__(self, ctx):


class REPLACE_IN_TEXT(Transform):
"""Replace various placeables in the translation with FTL placeables.
"""Replace various placeables in the translation with FTL.

The original placeables are defined as keys on the `replacements` dict.
For each key the value is defined as a list of FTL Expressions to be
interpolated.
For each key the value is defined as a FTL Pattern, Placeable,
TextElement or Expressions to be interpolated.
"""

def __init__(self, value, replacements):
Expand All @@ -141,7 +142,7 @@ def __init__(self, value, replacements):

def __call__(self, ctx):

# Only replace placeable which are present in the translation.
# Only replace placeables which are present in the translation.
replacements = {
key: evaluate(ctx, repl)
for key, repl in self.replacements.iteritems()
Expand All @@ -154,40 +155,43 @@ def __call__(self, ctx):
lambda x, y: self.value.find(x) - self.value.find(y)
)

# Used to reduce the `keys_in_order` list.
def replace(acc, cur):
"""Convert original placeables and text into FTL Nodes.

For each original placeable the translation will be partitioned
around it and the text before it will be converted into an
`FTL.TextElement` and the placeable will be replaced with its
replacement. The text following the placebale will be fed again to
the `replace` function.
"""

parts, rest = acc
before, key, after = rest.value.partition(cur)

placeable = FTL.Placeable(replacements[key])

# Return the elements found and converted so far, and the remaining
# text which hasn't been scanned for placeables yet.
return (
parts + [FTL.TextElement(before), placeable],
FTL.TextElement(after)
)

def is_non_empty(elem):
"""Used for filtering empty `FTL.TextElement` nodes out."""
return not isinstance(elem, FTL.TextElement) or len(elem.value)

# Start with an empty list of elements and the original translation.
init = ([], FTL.TextElement(self.value))
parts, tail = reduce(replace, keys_in_order, init)

# Explicitly concat the trailing part to get the full list of elements
# and filter out the empty ones.
elements = filter(is_non_empty, parts + [tail])
# A list of PatternElements built from the legacy translation and the
# FTL replacements. It may contain empty or adjacent TextElements.
parts = []
tail = self.value

# Convert original placeables and text into FTL Nodes. For each
# original placeable the translation will be partitioned around it and
# the text before it will be converted into an `FTL.TextElement` and
# the placeable will be replaced with its replacement.
for key in keys_in_order:
before, key, tail = tail.partition(key)

# The replacement value can be of different types.
replacement = replacements[key]
if isinstance(replacement, FTL.Pattern):
repl_elements = replacement.elements
elif isinstance(replacement, FTL.PatternElement):
repl_elements = [replacement]
elif isinstance(replacement, FTL.Expression):
repl_elements = [FTL.Placeable(replacement)]

parts.append(FTL.TextElement(before))
parts.extend(repl_elements)

# Dont' forget about the tail after the loop ends.
parts.append(FTL.TextElement(tail))

# Join adjacent TextElements.
elements = []
for elem_type, elems in itertools.groupby(parts, key=type):
if elem_type is FTL.TextElement:
text = FTL.TextElement(''.join(elem.value for elem in elems))
# And remove empty ones.
if len(text.value) > 0:
elements.append(text)
else:
elements.extend(elems)

return FTL.Pattern(elements)

Expand Down
7 changes: 5 additions & 2 deletions fluent/syntax/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,15 @@ def __init__(self, elements, **kwargs):
super(Pattern, self).__init__(**kwargs)
self.elements = elements

class TextElement(SyntaxNode):
class PatternElement(SyntaxNode):
pass

class TextElement(PatternElement):
def __init__(self, value, **kwargs):
super(TextElement, self).__init__(**kwargs)
self.value = value

class Placeable(SyntaxNode):
class Placeable(PatternElement):
def __init__(self, expression, **kwargs):
super(Placeable, self).__init__(**kwargs)
self.expression = expression
Expand Down
2 changes: 2 additions & 0 deletions tests/migrate/test_concat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@


class MockContext(unittest.TestCase):
maxDiff = None

def get_source(self, path, key):
# Ignore path (test.properties) and get translations from self.strings
# defined in setUp.
Expand Down
68 changes: 68 additions & 0 deletions tests/migrate/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@


class MockContext(unittest.TestCase):
maxDiff = None

def get_source(self, path, key):
# Ignore path (test.properties) and get translations from self.strings
# defined in setUp.
Expand Down Expand Up @@ -148,6 +150,72 @@ def test_replace_last(self):
''')
)

def test_replace_with_placeable(self):
msg = FTL.Message(
FTL.Identifier(u'hello'),
value=REPLACE(
'test.properties',
'hello',
{
'#1': FTL.Placeable(
EXTERNAL_ARGUMENT('user')
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work also with just EXTERNAL_ARGUMENT('user')?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all other tests in this file are written that way :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
)
)

self.assertEqual(
evaluate(self, msg).to_json(),
ftl_message_to_json('''
hello = Hello, { $user }!
''')
)

def test_replace_with_text_element(self):
msg = FTL.Message(
FTL.Identifier(u'hello'),
value=REPLACE(
'test.properties',
'hello',
{
'#1': FTL.TextElement('you')
}
)
)

self.assertEqual(
evaluate(self, msg).to_json(),
ftl_message_to_json('''
hello = Hello, you!
''')
)

def test_replace_with_pattern(self):
msg = FTL.Message(
FTL.Identifier(u'hello'),
value=REPLACE(
'test.properties',
'hello',
{
'#1': FTL.Pattern(
elements=[
FTL.TextElement('<img> '),
FTL.Placeable(
EXTERNAL_ARGUMENT('user')
)
]
)
}
)
)

self.assertEqual(
evaluate(self, msg).to_json(),
ftl_message_to_json('''
hello = Hello, <img> { $user }!
''')
)


if __name__ == '__main__':
unittest.main()