diff --git a/.gitattributes b/.gitattributes index 74346c43..1b6a9d71 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -tests/syntax/fixtures_reference/crlf.ftl eol=crlf -tests/syntax/fixtures_reference/cr.ftl eol=cr -tests/syntax/fixtures_structure/crlf.ftl eol=crlf +fluent.syntax/tests/syntax/fixtures_reference/crlf.ftl eol=crlf +fluent.syntax/tests/syntax/fixtures_reference/cr.ftl eol=cr +fluent.syntax/tests/syntax/fixtures_structure/crlf.ftl eol=crlf diff --git a/.gitignore b/.gitignore index 5e8e3e92..302ea433 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .tox *.pyc .eggs/ -fluent.egg-info/ +*.egg-info/ diff --git a/.travis.yml b/.travis.yml index d31d1eb5..5f1d4957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,14 @@ python: - "2.7" - "3.5" - "3.6" + - "pypy" + - "pypy3" - "nightly" +env: + - PACKAGE=fluent.syntax + - PACKAGE=fluent.runtime install: pip install tox-travis -script: tox +script: cd $PACKAGE; tox notifications: irc: channels: diff --git a/README.md b/README.md index 85185d4a..aac51b4c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,251 @@ you're a tool author you may be interested in the formal [EBNF grammar][]. [EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec +Installation +------------ + +python-fluent consists of two packages: + +* `fluent.syntax` - includes AST classes and parser. Most end users will not + need this directly. Documentation coming soon! + + To install: + + pip install fluent.syntax + + +* `fluent.runtime` - methods for generating translations from FTL files. + Documentation below. + + To install: + + pip install fluent.runtime + + (The correct version of ``fluent.syntax`` will be installed automatically) + + +PyPI also contains an old `fluent` package which is an older version of just +`fluent.syntax`. + +Usage +----- + +To generate translations using the ``fluent.runtime`` package, you start with +the `FluentBundle` class: + + >>> from fluent.runtime import FluentBundle + +You pass a list of locales to the constructor - the first being the desired +locale, with fallbacks after that: + + >>> bundle = FluentBundle(["en-US"]) + + +You must then add messages. These would normally come from a `.ftl` file stored +on disk, here we will just add them directly: + + >>> bundle.add_messages(""" + ... welcome = Welcome to this great app! + ... greet-by-name = Hello, { $name }! + ... """) + +To generate translations, use the `format` method, passing a message ID and an +optional dictionary of substitution parameters. If the the message ID is not +found, a `LookupError` is raised. Otherwise, as per the Fluent philosophy, the +implementation tries hard to recover from any formatting errors and generate the +most human readable representation of the value. The `format` method therefore +returns a tuple containing `(translated string, errors)`, as below. + + >>> translated, errs = bundle.format('welcome') + >>> translated + "Welcome to this great app!" + >>> errs + [] + + >>> translated, errs = bundle.format('greet-by-name', {'name': 'Jane'}) + >>> translated + 'Hello, \u2068Jane\u2069!' + + >>> translated, errs = bundle.format('greet-by-name', {}) + >>> translated + 'Hello, \u2068name\u2069!' + >>> errs + [FluentReferenceError('Unknown external: name')] + +You will notice the extra characters `\u2068` and `\u2069` in the output. These +are Unicode bidi isolation characters that help to ensure that the interpolated +strings are handled correctly in the situation where the text direction of the +substitution might not match the text direction of the localized text. These +characters can be disabled if you are sure that is not possible for your app by +passing `use_isolating=False` to the `FluentBundle` constructor. + +Python 2 +-------- + +The above examples assume Python 3. Since Fluent uses unicode everywhere +internally (and doesn't accept bytestrings), if you are using Python 2 you will +need to make adjustments to the above example code. Either add `u` unicode +literal markers to strings or add this at the top of the module or the start of +your repl session: + + from __future__ import unicode_literals + + +Numbers +------- + +When rendering translations, Fluent passes any numeric arguments (int or float) +through locale-aware formatting functions: + + >>> bundle.add_messages("show-total-points = You have { $points } points.") + >>> val, errs = bundle.format("show-total-points", {'points': 1234567}) + >>> val + 'You have 1,234,567 points.' + + +You can specify your own formatting options on the arguments passed in by +wrapping your numeric arguments with `fluent.runtime.types.fluent_number`: + + >>> from fluent.runtime.types import fluent_number + >>> points = fluent_number(1234567, useGrouping=False) + >>> bundle.format("show-total-points", {'points': points})[0] + 'You have 1234567 points.' + + >>> amount = fluent_number(1234.56, style="currency", currency="USD") + >>> bundle.add_messages("your-balance = Your balance is { $amount }") + >>> bundle.format("your-balance", {'amount': amount})[0] + 'Your balance is $1,234.56' + +Thee options available are defined in the Fluent spec for +[NUMBER](https://projectfluent.org/fluent/guide/functions.html#number). Some of +these options can also be defined in the FTL files, as described in the Fluent +spec, and the options will be merged. + +Date and time +------------- + +Python `datetime.datetime` and `datetime.date` objects are also passed through +locale aware functions: + + >>> from datetime import date + >>> bundle.add_messages("today-is = Today is { $today }") + >>> val, errs = bundle.format("today-is", {"today": date.today() }) + >>> val + 'Today is Jun 16, 2018' + +You can explicitly call the `DATETIME` builtin to specify options: + + >>> bundle.add_messages('today-is = Today is { DATETIME($today, dateStyle: "short") }') + +See the [DATETIME +docs](https://projectfluent.org/fluent/guide/functions.html#datetime). However, +currently the only supported options to `DATETIME` are: + +* `timeZone` +* `dateStyle` and `timeStyle` which are [proposed + additions](https://github.com/tc39/proposal-ecma402-datetime-style) to the ECMA i18n spec. + +To specify options from Python code, use `fluent.runtime.types.fluent_date`: + + >>> from fluent.runtime.types import fluent_date + >>> today = date.today() + >>> short_today = fluent_date(today, dateStyle='short') + >>> val, errs = bundle.format("today-is", {"today": short_today }) + >>> val + 'Today is 6/17/18' + +You can also specify timezone for displaying `datetime` objects in two ways: + +* Create timezone aware `datetime` objects, and pass these to the `format` call + e.g.: + + >>> import pytz + >>> from datetime import datetime + >>> utcnow = datime.utcnow().replace(tzinfo=pytz.utc) + >>> moscow_timezone = pytz.timezone('Europe/Moscow') + >>> now_in_moscow = utcnow.astimezone(moscow_timezone) + +* Or, use timezone naive `datetime` objects, or ones with a UTC timezone, and + pass the `timeZone` argument to `fluent_date` as a string: + + >>> utcnow = datetime.utcnow() + >>> utcnow + datetime.datetime(2018, 6, 17, 12, 15, 5, 677597) + + >>> bundle.add_messages("now-is = Now is { $now }") + >>> val, errs = bundle.format("now-is", + ... {"now": fluent_date(utcnow, + ... timeZone="Europe/Moscow", + ... dateStyle="medium", + ... timeStyle="medium")}) + >>> val + 'Now is Jun 17, 2018, 3:15:05 PM' + + +Custom functions +---------------- + +You can add functions to the ones available to FTL authors by passing +a `functions` dictionary to the `FluentBundle` constructor: + + + >>> import platform + >>> def os_name(): + ... """Returns linux/mac/windows/other""" + ... return {'Linux': 'linux', + ... 'Darwin': 'mac', + ... 'Windows': 'windows'}.get(platform.system(), 'other') + + >>> bundle = FluentBundle(['en-US'], functions={'OS': os_name}) + >>> bundle.add_messages(""" + ... welcome = { OS() -> + ... [linux] Welcome to Linux + ... [mac] Welcome to Mac + ... [windows] Welcome to Windows + ... *[other] Welcome + ... } + ... """) + >>> print(bundle.format('welcome')[0] + Welcome to Linux + +These functions can accept positioal and keyword arguments (like the `NUMBER` +and `DATETIME` builtins), and in this case must accept the following types of +arguments: + +* unicode strings (i.e. `unicode` on Python 2, `str` on Python 3) +* `fluent.runtime.types.FluentType` subclasses, namely: + * `FluentNumber` - `int`, `float` or `Decimal` objects passed in externally, + or expressed as literals, are wrapped in these. Note that these objects also + subclass builtin `int`, `float` or `Decimal`, so can be used as numbers in + the normal way. + * `FluentDateType` - `date` or `datetime` objects passed in are wrapped in + these. Again, these classes also subclass `date` or `datetime`, and can be + used as such. + * `FluentNone` - in error conditions, such as a message referring to an argument + that hasn't been passed in, objects of this type are passed in. + +Custom functions should not throw errors, but return `FluentNone` instances to +indicate an error or missing data. Otherwise they should return unicode strings, +or instances of a `FluentType` subclass as above. + + +Known limitations and bugs +-------------------------- + +* We do not yet support `NUMBER(..., currencyDisplay="name")` - see [this python-babel + pull request](https://github.com/python-babel/babel/pull/585) which needs to + be merged and released. + +* Most options to `DATETIME` are not yet supported. See the [MDN docs for + Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat), + the [ECMA spec for + BasicFormatMatcher](http://www.ecma-international.org/ecma-402/1.0/#BasicFormatMatcher) + and the [Intl.js + polyfill](https://github.com/andyearnshaw/Intl.js/blob/master/src/12.datetimeformat.js). + +Help with the above would be welcome! + + Discuss ------- diff --git a/fluent/__init__.py b/fluent.runtime/fluent/__init__.py similarity index 100% rename from fluent/__init__.py rename to fluent.runtime/fluent/__init__.py diff --git a/fluent.runtime/fluent/runtime/__init__.py b/fluent.runtime/fluent/runtime/__init__.py new file mode 100644 index 00000000..55df3368 --- /dev/null +++ b/fluent.runtime/fluent/runtime/__init__.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import, unicode_literals + +import babel +import babel.numbers +import babel.plural + +from fluent.syntax import FluentParser +from fluent.syntax.ast import Message, Term + +from .builtins import BUILTINS +from .resolver import resolve + + +class FluentBundle(object): + """ + Message contexts are single-language stores of translations. They are + responsible for parsing translation resources in the Fluent syntax and can + format translation units (entities) to strings. + + Always use `FluentBundle.format` to retrieve translation units from + a context. Translations can contain references to other entities or + external arguments, conditional logic in form of select expressions, traits + which describe their grammatical features, and can use Fluent builtins. + See the documentation of the Fluent syntax for more information. + """ + + def __init__(self, locales, functions=None, use_isolating=True): + self.locales = locales + _functions = BUILTINS.copy() + if functions: + _functions.update(functions) + self._functions = _functions + self._use_isolating = use_isolating + self._messages_and_terms = {} + self._babel_locale = self._get_babel_locale() + self._plural_form = babel.plural.to_python(self._babel_locale.plural_form) + + def add_messages(self, source): + parser = FluentParser() + resource = parser.parse(source) + # TODO - warn/error about duplicates + for item in resource.body: + if isinstance(item, (Message, Term)): + if item.id.name not in self._messages_and_terms: + self._messages_and_terms[item.id.name] = item + + def has_message(self, message_id): + if message_id.startswith('-'): + return False + return message_id in self._messages_and_terms + + def format(self, message_id, args=None): + message = self._get_message(message_id) + if args is None: + args = {} + return resolve(self, message, args) + + def _get_message(self, message_id): + if message_id.startswith('-'): + raise LookupError(message_id) + if '.' in message_id: + name, attr_name = message_id.split('.', 1) + msg = self._messages_and_terms[name] + for attribute in msg.attributes: + if attribute.id.name == attr_name: + return attribute.value + raise LookupError(message_id) + else: + return self._messages_and_terms[message_id] + + def _get_babel_locale(self): + for l in self.locales: + try: + return babel.Locale.parse(l.replace('-', '_')) + except babel.UnknownLocaleError: + continue + # TODO - log error + return babel.Locale.default() diff --git a/fluent.runtime/fluent/runtime/builtins.py b/fluent.runtime/fluent/runtime/builtins.py new file mode 100644 index 00000000..3b8bd4e9 --- /dev/null +++ b/fluent.runtime/fluent/runtime/builtins.py @@ -0,0 +1,10 @@ +from .types import fluent_date, fluent_number + +NUMBER = fluent_number +DATETIME = fluent_date + + +BUILTINS = { + 'NUMBER': NUMBER, + 'DATETIME': DATETIME, +} diff --git a/fluent.runtime/fluent/runtime/errors.py b/fluent.runtime/fluent/runtime/errors.py new file mode 100644 index 00000000..5c25da42 --- /dev/null +++ b/fluent.runtime/fluent/runtime/errors.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import, unicode_literals + + +class FluentFormatError(ValueError): + def __eq__(self, other): + return ((other.__class__ == self.__class__) and + other.args == self.args) + + +class FluentReferenceError(FluentFormatError): + pass + + +class FluentCyclicReferenceError(FluentFormatError): + pass diff --git a/fluent.runtime/fluent/runtime/resolver.py b/fluent.runtime/fluent/runtime/resolver.py new file mode 100644 index 00000000..19adc241 --- /dev/null +++ b/fluent.runtime/fluent/runtime/resolver.py @@ -0,0 +1,386 @@ +from __future__ import absolute_import, unicode_literals + +from datetime import date, datetime +from decimal import Decimal + +import attr +import six + +from fluent.syntax.ast import (AttributeExpression, CallExpression, Message, + MessageReference, NumberLiteral, Pattern, + Placeable, SelectExpression, StringLiteral, Term, + TermReference, TextElement, VariableReference, + VariantExpression, VariantList, Identifier) + +from .errors import FluentCyclicReferenceError, FluentReferenceError +from .types import FluentDateType, FluentNone, FluentNumber, fluent_date, fluent_number +from .utils import numeric_to_native + +try: + from functools import singledispatch +except ImportError: + # Python < 3.4 + from singledispatch import singledispatch + + +text_type = six.text_type + +# Prevent expansion of too long placeables, for memory DOS protection +MAX_PART_LENGTH = 2500 + +# Prevent messages with too many sub parts, for CPI DOS protection +MAX_PARTS = 1000 + + +# Unicode bidi isolation characters. +FSI = "\u2068" +PDI = "\u2069" + + +@attr.s +class ResolverEnvironment(object): + context = attr.ib() + args = attr.ib() + errors = attr.ib() + dirty = attr.ib(factory=set) + part_count = attr.ib(default=0) + + +def resolve(context, message, args): + """ + Given a FluentBundle, a Message instance and some arguments, + resolve the message to a string. + + This is the normal entry point for this module. + """ + errors = [] + env = ResolverEnvironment(context=context, + args=args, + errors=errors) + return fully_resolve(message, env), errors + + +def fully_resolve(expr, env): + """ + Fully resolve an expression to a string + """ + # This differs from 'handle' in that 'handle' will often return non-string + # objects, even if a string could have been returned, to allow for further + # handling of that object e.g. attributes of messages. fully_resolve is + # only used when we must have a string. + retval = handle(expr, env) + if isinstance(retval, text_type): + return retval + else: + return fully_resolve(retval, env) + + +@singledispatch +def handle(expr, env): + raise NotImplementedError("Cannot handle object of type {0}" + .format(type(expr).__name__)) + + +@handle.register(Message) +def handle_message(message, env): + return handle(message.value, env) + + +@handle.register(Term) +def handle_term(term, env): + return handle(term.value, env) + + +@handle.register(Pattern) +def handle_pattern(pattern, env): + if pattern in env.dirty: + env.errors.append(FluentCyclicReferenceError("Cyclic reference")) + return FluentNone() + + env.dirty.add(pattern) + + parts = [] + use_isolating = env.context._use_isolating and len(pattern.elements) > 1 + + for element in pattern.elements: + env.part_count += 1 + if env.part_count > MAX_PARTS: + if env.part_count == MAX_PARTS + 1: + # Only append an error once. + env.errors.append(ValueError("Too many parts in message (> {0}), " + "aborting.".format(MAX_PARTS))) + parts.append(fully_resolve(FluentNone(), env)) + break + + if isinstance(element, TextElement): + # shortcut deliberately omits the FSI/PDI chars here. + parts.append(element.value) + continue + + part = fully_resolve(element, env) + if use_isolating: + parts.append(FSI) + if len(part) > MAX_PART_LENGTH: + env.errors.append(ValueError( + "Too many characters in part, " + "({0}, max allowed is {1})".format(len(part), + MAX_PART_LENGTH))) + part = part[:MAX_PART_LENGTH] + parts.append(part) + if use_isolating: + parts.append(PDI) + retval = "".join(parts) + env.dirty.remove(pattern) + return retval + + +@handle.register(TextElement) +def handle_text_element(text_element, env): + return text_element.value + + +@handle.register(Placeable) +def handle_placeable(placeable, env): + return handle(placeable.expression, env) + + +@handle.register(StringLiteral) +def handle_string_expression(string_expression, env): + return string_expression.value + + +@handle.register(NumberLiteral) +def handle_number_expression(number_expression, env): + return numeric_to_native(number_expression.value) + + +@handle.register(MessageReference) +def handle_message_reference(message_reference, env): + name = message_reference.id.name + return handle(lookup_reference(name, env), env) + + +@handle.register(TermReference) +def handle_term_reference(term_reference, env): + name = term_reference.id.name + return handle(lookup_reference(name, env), env) + + +def lookup_reference(name, env): + message = None + try: + message = env.context._messages_and_terms[name] + except LookupError: + if name.startswith("-"): + env.errors.append( + FluentReferenceError("Unknown term: {0}" + .format(name))) + else: + env.errors.append( + FluentReferenceError("Unknown message: {0}" + .format(name))) + if message is None: + message = FluentNone(name) + + return message + + +@handle.register(FluentNone) +def handle_fluent_none(none, env): + return none.format(env.context._babel_locale) + + +@handle.register(type(None)) +def handle_none(none, env): + # We raise the same error type here as when a message is completely missing. + raise LookupError("Message body not defined") + + +@handle.register(VariableReference) +def handle_variable_reference(argument, env): + name = argument.id.name + try: + arg_val = env.args[name] + except LookupError: + env.errors.append( + FluentReferenceError("Unknown external: {0}".format(name))) + return FluentNone(name) + + if isinstance(arg_val, + (int, float, Decimal, + date, datetime, + text_type)): + return arg_val + env.errors.append(TypeError("Unsupported external type: {0}, {1}" + .format(name, type(arg_val)))) + return FluentNone(name) + + +@handle.register(AttributeExpression) +def handle_attribute_expression(attribute, env): + parent_id = attribute.ref.id.name + attr_name = attribute.name.name + message = lookup_reference(parent_id, env) + if isinstance(message, FluentNone): + return message + + for message_attr in message.attributes: + if message_attr.id.name == attr_name: + return handle(message_attr.value, env) + + env.errors.append( + FluentReferenceError("Unknown attribute: {0}.{1}" + .format(parent_id, attr_name))) + return handle(message, env) + + +@handle.register(VariantList) +def handle_variant_list(variant_list, env): + return select_from_variant_list(variant_list, env, None) + + +def select_from_variant_list(variant_list, env, key): + found = None + for variant in variant_list.variants: + if variant.default: + default = variant + if key is None: + # We only want the default + break + + compare_value = handle(variant.key, env) + if match(key, compare_value, env): + found = variant + break + + if found is None: + if (key is not None and not isinstance(key, FluentNone)): + env.errors.append(FluentReferenceError("Unknown variant: {0}" + .format(key))) + found = default + if found is None: + return FluentNone() + else: + return handle(found.value, env) + + +@handle.register(SelectExpression) +def handle_select_expression(expression, env): + key = handle(expression.selector, env) + return select_from_select_expression(expression, env, + key=key) + + +def select_from_select_expression(expression, env, key): + default = None + found = None + for variant in expression.variants: + if variant.default: + default = variant + + compare_value = handle(variant.key, env) + if match(key, compare_value, env): + found = variant + break + + if found is None: + found = default + if found is None: + return FluentNone() + else: + return handle(found.value, env) + + +def is_number(val): + return isinstance(val, (int, float)) + + +def match(val1, val2, env): + if val1 is None or isinstance(val1, FluentNone): + return False + if val2 is None or isinstance(val2, FluentNone): + return False + if is_number(val1): + if not is_number(val2): + # Could be plural rule match + return env.context._plural_form(val1) == val2 + else: + if is_number(val2): + return match(val2, val1, env) + + return val1 == val2 + + +@handle.register(Identifier) +def handle_indentifier(identifier, env): + return identifier.name + + +@handle.register(VariantExpression) +def handle_variant_expression(expression, env): + message = lookup_reference(expression.ref.id.name, env) + if isinstance(message, FluentNone): + return message + + # TODO What to do if message is not a VariantList? + # Need test at least. + assert isinstance(message.value, VariantList) + + variant_name = expression.key.name + return select_from_variant_list(message.value, + env, + variant_name) + + +@handle.register(CallExpression) +def handle_call_expression(expression, env): + function_name = expression.callee.name + try: + function = env.context._functions[function_name] + except LookupError: + env.errors.append(FluentReferenceError("Unknown function: {0}" + .format(function_name))) + return FluentNone(function_name + "()") + + args = [handle(arg, env) for arg in expression.positional] + kwargs = {kwarg.name.name: handle(kwarg.value, env) for kwarg in expression.named} + try: + return function(*args, **kwargs) + except Exception as e: + env.errors.append(e) + return FluentNone(function_name + "()") + + +@handle.register(FluentNumber) +def handle_fluent_number(number, env): + return number.format(env.context._babel_locale) + + +@handle.register(int) +def handle_int(integer, env): + return fluent_number(integer).format(env.context._babel_locale) + + +@handle.register(float) +def handle_float(f, env): + return fluent_number(f).format(env.context._babel_locale) + + +@handle.register(Decimal) +def handle_decimal(d, env): + return fluent_number(d).format(env.context._babel_locale) + + +@handle.register(FluentDateType) +def handle_fluent_date_type(d, env): + return d.format(env.context._babel_locale) + + +@handle.register(date) +def handle_date(d, env): + return fluent_date(d).format(env.context._babel_locale) + + +@handle.register(datetime) +def handle_datetime(d, env): + return fluent_date(d).format(env.context._babel_locale) diff --git a/fluent.runtime/fluent/runtime/types.py b/fluent.runtime/fluent/runtime/types.py new file mode 100644 index 00000000..228d4bf6 --- /dev/null +++ b/fluent.runtime/fluent/runtime/types.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import warnings +from datetime import date, datetime +from decimal import Decimal + +import attr +import pytz +from babel.dates import format_date, format_time, get_datetime_format, get_timezone +from babel.numbers import NumberPattern, parse_pattern + +FORMAT_STYLE_DECIMAL = "decimal" +FORMAT_STYLE_CURRENCY = "currency" +FORMAT_STYLE_PERCENT = "percent" +FORMAT_STYLE_OPTIONS = set([ + FORMAT_STYLE_DECIMAL, + FORMAT_STYLE_CURRENCY, + FORMAT_STYLE_PERCENT, +]) + +CURRENCY_DISPLAY_SYMBOL = "symbol" +CURRENCY_DISPLAY_CODE = "code" +CURRENCY_DISPLAY_NAME = "name" +CURRENCY_DISPLAY_OPTIONS = set([ + CURRENCY_DISPLAY_SYMBOL, + CURRENCY_DISPLAY_CODE, + CURRENCY_DISPLAY_NAME, +]) + +DATE_STYLE_OPTIONS = set([ + "full", + "long", + "medium", + "short", + None, +]) + +TIME_STYLE_OPTIONS = set([ + "full", + "long", + "medium", + "short", + None, +]) + + +class FluentType(object): + def format(self, locale): + raise NotImplementedError() + + +class FluentNone(FluentType): + def __init__(self, name=None): + self.name = name + + def __eq__(self, other): + return isinstance(other, FluentNone) and self.name == other.name + + def format(self, locale): + return self.name or "???" + + +@attr.s +class NumberFormatOptions(object): + # We follow the Intl.NumberFormat parameter names here, + # rather than using underscores as per PEP8, so that + # we can stick to Fluent spec more easily. + + # See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat + style = attr.ib(default=FORMAT_STYLE_DECIMAL, + validator=attr.validators.in_(FORMAT_STYLE_OPTIONS)) + currency = attr.ib(default=None) + currencyDisplay = attr.ib(default=CURRENCY_DISPLAY_SYMBOL, + validator=attr.validators.in_(CURRENCY_DISPLAY_OPTIONS)) + useGrouping = attr.ib(default=True) + minimumIntegerDigits = attr.ib(default=None) + minimumFractionDigits = attr.ib(default=None) + maximumFractionDigits = attr.ib(default=None) + minimumSignificantDigits = attr.ib(default=None) + maximumSignificantDigits = attr.ib(default=None) + + +class FluentNumber(object): + + default_number_format_options = NumberFormatOptions() + + def __new__(cls, + value, + **kwargs): + self = super(FluentNumber, cls).__new__(cls, value) + return self._init(value, kwargs) + + def _init(self, value, kwargs): + self.options = merge_options(NumberFormatOptions, + getattr(value, 'options', self.default_number_format_options), + kwargs) + + if self.options.style == FORMAT_STYLE_CURRENCY and self.options.currency is None: + raise ValueError("currency must be provided") + + return self + + def format(self, locale): + if self.options.style == FORMAT_STYLE_DECIMAL: + base_pattern = locale.decimal_formats.get(None) + pattern = self._apply_options(base_pattern) + return pattern.apply(self, locale) + elif self.options.style == FORMAT_STYLE_PERCENT: + base_pattern = locale.percent_formats.get(None) + pattern = self._apply_options(base_pattern) + return pattern.apply(self, locale) + elif self.options.style == FORMAT_STYLE_CURRENCY: + base_pattern = locale.currency_formats['standard'] + pattern = self._apply_options(base_pattern) + return pattern.apply(self, locale, currency=self.options.currency) + + def _apply_options(self, pattern): + # We are essentially trying to copy the + # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat + # API using Babel number formatting routines, which is slightly awkward + # but not too bad as they are both based on Unicode standards. + + # The easiest route is to start from the existing NumberPattern, and + # then change its attributes so that Babel's number formatting routines + # do the right thing. The NumberPattern.pattern string then becomes + # incorrect, but it is not used when formatting, it is only used + # initially to set the other attributes. + pattern = clone_pattern(pattern) + if not self.options.useGrouping: + pattern.grouping = _UNGROUPED_PATTERN.grouping + if self.options.style == FORMAT_STYLE_CURRENCY: + if self.options.currencyDisplay == CURRENCY_DISPLAY_CODE: + # Not sure of the correct algorithm here, but this seems to + # work: + def replacer(s): + return s.replace("¤", "¤¤") + pattern.suffix = (replacer(pattern.suffix[0]), + replacer(pattern.suffix[1])) + pattern.prefix = (replacer(pattern.prefix[0]), + replacer(pattern.prefix[1])) + elif self.options.currencyDisplay == CURRENCY_DISPLAY_NAME: + # No support for this yet - see + # https://github.com/python-babel/babel/issues/578 But it's + # better to display something than crash or a generic fallback + # string, so we just issue a warning and carry on for now. + warnings.warn("Unsupported currencyDisplayValue {0}, falling back to {1}" + .format(CURRENCY_DISPLAY_NAME, + CURRENCY_DISPLAY_SYMBOL)) + if (self.options.minimumSignificantDigits is not None + or self.options.maximumSignificantDigits is not None): + # This triggers babel routines into 'significant digits' mode: + pattern.pattern = '@' + # We then manually set int_prec, and leave the rest as they are. + min_digits = (1 if self.options.minimumSignificantDigits is None + else self.options.minimumSignificantDigits) + max_digits = (min_digits if self.options.maximumSignificantDigits is None + else self.options.maximumSignificantDigits) + pattern.int_prec = (min_digits, max_digits) + else: + if self.options.minimumIntegerDigits is not None: + pattern.int_prec = (self.options.minimumIntegerDigits, pattern.int_prec[1]) + if self.options.minimumFractionDigits is not None: + pattern.frac_prec = (self.options.minimumFractionDigits, pattern.frac_prec[1]) + if self.options.maximumFractionDigits is not None: + pattern.frac_prec = (pattern.frac_prec[0], self.options.maximumFractionDigits) + + return pattern + + +def merge_options(options_class, base, kwargs): + """ + Given an 'options_class', an optional 'base' object to copy from, + and some keyword arguments, create a new options instance + """ + if base is not None and not kwargs: + # We can safely re-use base, because we don't + # mutate options objects outside this function. + return base + + retval = options_class() + + if base is not None: + # We only copy values in `__dict__` to avoid class attributes. + retval.__dict__.update(base.__dict__) + + # Use the options_class constructor because it might + # have validators defined for the fields. + kwarg_options = options_class(**kwargs) + # Then merge, using only the ones explicitly given as keyword params. + for k in kwargs.keys(): + setattr(retval, k, getattr(kwarg_options, k)) + + return retval + + +# We want types that inherit from both FluentNumber and a native type, +# so that: +# +# 1) developers can just pass native types if they don't want to specify +# options, and fluent should handle these the same internally. +# +# 2) if they are using functions in messages, these can be passed FluentNumber +# instances in place of a native type and will work just the same without +# modification (in most cases). + +class FluentInt(FluentNumber, int): + pass + + +class FluentFloat(FluentNumber, float): + pass + + +class FluentDecimal(FluentNumber, Decimal): + pass + + +def fluent_number(number, **kwargs): + if isinstance(number, FluentNumber) and not kwargs: + return number + if isinstance(number, int): + return FluentInt(number, **kwargs) + elif isinstance(number, float): + return FluentFloat(number, **kwargs) + elif isinstance(number, Decimal): + return FluentDecimal(number, **kwargs) + elif isinstance(number, FluentNone): + return number + else: + raise TypeError("Can't use fluent_number with object {0} for type {1}" + .format(number, type(number))) + + +_UNGROUPED_PATTERN = parse_pattern("#0") + + +def clone_pattern(pattern): + return NumberPattern(pattern.pattern, + pattern.prefix, + pattern.suffix, + pattern.grouping, + pattern.int_prec, + pattern.frac_prec, + pattern.exp_prec, + pattern.exp_plus) + + +@attr.s +class DateFormatOptions(object): + # Parameters. + # See https://projectfluent.org/fluent/guide/functions.html#datetime + + # Developer only + timeZone = attr.ib(default=None) + + # Other + hour12 = attr.ib(default=None) + weekday = attr.ib(default=None) + era = attr.ib(default=None) + year = attr.ib(default=None) + month = attr.ib(default=None) + day = attr.ib(default=None) + hour = attr.ib(default=None) + minute = attr.ib(default=None) + second = attr.ib(default=None) + timeZoneName = attr.ib(default=None) + + # See https://github.com/tc39/proposal-ecma402-datetime-style + dateStyle = attr.ib(default=None, + validator=attr.validators.in_(DATE_STYLE_OPTIONS)) + timeStyle = attr.ib(default=None, + validator=attr.validators.in_(TIME_STYLE_OPTIONS)) + + +_SUPPORTED_DATETIME_OPTIONS = ['dateStyle', 'timeStyle', 'timeZone'] + + +class FluentDateType(object): + def _init(self, dt_obj, kwargs): + if 'timeStyle' in kwargs and not isinstance(self, datetime): + raise TypeError("timeStyle option can only be specified for datetime instances, not date instance") + + self.options = merge_options(DateFormatOptions, + getattr(dt_obj, 'options', None), + kwargs) + for k in kwargs: + if k not in _SUPPORTED_DATETIME_OPTIONS: + warnings.warn("FluentDateType option {0} is not yet supported".format(k)) + + def format(self, locale): + if isinstance(self, datetime): + selftz = _ensure_datetime_tzinfo(self, tzinfo=self.options.timeZone) + else: + selftz = self + + if self.options.dateStyle is None and self.options.timeStyle is None: + return format_date(selftz, format='medium', locale=locale) + elif self.options.dateStyle is None and self.options.timeStyle is not None: + return format_time(selftz, format=self.options.timeStyle, locale=locale) + elif self.options.dateStyle is not None and self.options.timeStyle is None: + return format_date(selftz, format=self.options.dateStyle, locale=locale) + else: + # Both date and time. Logic copied from babel.dates.format_datetime, + # with modifications. + # Which datetime format do we pick? We arbitrarily pick dateStyle. + + return (get_datetime_format(self.options.dateStyle, locale=locale) + .replace("'", "") + .replace('{0}', format_time(selftz, self.options.timeStyle, tzinfo=None, + locale=locale)) + .replace('{1}', format_date(selftz, self.options.dateStyle, locale=locale)) + ) + + +def _ensure_datetime_tzinfo(dt, tzinfo=None): + """ + Ensure the datetime passed has an attached tzinfo. + """ + # Adapted from babel's function. + if dt.tzinfo is None: + dt = dt.replace(tzinfo=pytz.UTC) + if tzinfo is not None: + dt = dt.astimezone(get_timezone(tzinfo)) + if hasattr(tzinfo, 'normalize'): # pytz + dt = tzinfo.normalize(datetime) + return dt + + +class FluentDate(FluentDateType, date): + def __new__(cls, + dt_obj, + **kwargs): + self = super(FluentDate, cls).__new__( + cls, + dt_obj.year, dt_obj.month, dt_obj.day) + self._init(dt_obj, kwargs) + return self + + +class FluentDateTime(FluentDateType, datetime): + def __new__(cls, + dt_obj, + **kwargs): + self = super(FluentDateTime, cls).__new__( + cls, + dt_obj.year, dt_obj.month, dt_obj.day, + dt_obj.hour, dt_obj.minute, dt_obj.second, + dt_obj.microsecond, tzinfo=dt_obj.tzinfo) + + self._init(dt_obj, kwargs) + return self + + +def fluent_date(dt, **kwargs): + if isinstance(dt, FluentDateType) and not kwargs: + return dt + if isinstance(dt, datetime): + return FluentDateTime(dt, **kwargs) + elif isinstance(dt, date): + return FluentDate(dt, **kwargs) + elif isinstance(dt, FluentNone): + return dt + else: + raise TypeError("Can't use fluent_date with object {0} of type {1}" + .format(dt, type(dt))) diff --git a/fluent.runtime/fluent/runtime/utils.py b/fluent.runtime/fluent/runtime/utils.py new file mode 100644 index 00000000..1f67dd26 --- /dev/null +++ b/fluent.runtime/fluent/runtime/utils.py @@ -0,0 +1,11 @@ +def numeric_to_native(val): + """ + Given a numeric string (as defined by fluent spec), + return an int or float + """ + # val matches this EBNF: + # '-'? [0-9]+ ('.' [0-9]+)? + if '.' in val: + return float(val) + else: + return int(val) diff --git a/fluent.runtime/runtests.py b/fluent.runtime/runtests.py new file mode 100755 index 00000000..191991a2 --- /dev/null +++ b/fluent.runtime/runtests.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# This file is symlinked from both fluent.runtime and fluent.syntax directories + +import argparse +import subprocess +import sys + +parser = argparse.ArgumentParser( + description="Run the test suite, or some tests") +parser.add_argument('--coverage', "-c", action='store_true', + help="Run with 'coverage'") +parser.add_argument('test', type=str, nargs="*", + help="Dotted path to a test module, case or method") + +args = parser.parse_args() + +cmd = ["-m", "unittest"] + +if args.test: + cmd.extend(args.test) +else: + cmd.extend(["discover", "-t", ".", "-s", "tests"]) + +if args.coverage: + cmd = ["-m", "coverage", "run"] + cmd + +cmd.insert(0, "python") + +sys.exit(subprocess.call(cmd)) diff --git a/fluent.runtime/setup.cfg b/fluent.runtime/setup.cfg new file mode 100644 index 00000000..02d416e8 --- /dev/null +++ b/fluent.runtime/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +universal=1 + +[flake8] +exclude=.tox +max-line-length=120 + +[isort] +line_length=120 +skip_glob=.tox +not_skip=__init__.py diff --git a/fluent.runtime/setup.py b/fluent.runtime/setup.py new file mode 100644 index 00000000..fe7e6b8e --- /dev/null +++ b/fluent.runtime/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +from setuptools import setup +import sys + +if sys.version_info < (3, 4): + extra_requires = ['singledispatch>=3.4'] +else: + # functools.singledispatch is in stdlib from Python 3.4 onwards. + extra_requires = [] + +setup(name='fluent.runtime', + version='0.1', + description='Localization library for expressive translations.', + long_description='See https://github.com/projectfluent/python-fluent/ for more info.', + author='Luke Plant', + author_email='L.Plant.98@cantab.net', + license='APL 2', + url='https://github.com/projectfluent/python-fluent', + keywords=['fluent', 'localization', 'l10n'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', + ], + packages=['fluent', 'fluent.runtime'], + install_requires=[ + 'fluent>=0.9,<0.10', + 'attrs', + 'babel', + 'pytz', + ] + extra_requires, + tests_require=['six'], + test_suite='tests' + ) diff --git a/tests/__init__.py b/fluent.runtime/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to fluent.runtime/tests/__init__.py diff --git a/tests/syntax/fixtures_behavior/empty_resource.ftl b/fluent.runtime/tests/format/__init__.py similarity index 100% rename from tests/syntax/fixtures_behavior/empty_resource.ftl rename to fluent.runtime/tests/format/__init__.py diff --git a/fluent.runtime/tests/format/test_arguments.py b/fluent.runtime/tests/format/test_arguments.py new file mode 100644 index 00000000..72b9e6f7 --- /dev/null +++ b/fluent.runtime/tests/format/test_arguments.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle + +from ..utils import dedent_ftl + + +class TestNumbersInValues(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo { $num } + bar = { foo } + baz = + .attr = Baz Attribute { $num } + qux = { "a" -> + *[a] Baz Variant A { $num } + } + """)) + + def test_can_be_used_in_the_message_value(self): + val, errs = self.ctx.format('foo', {'num': 3}) + self.assertEqual(val, 'Foo 3') + self.assertEqual(len(errs), 0) + + def test_can_be_used_in_the_message_value_which_is_referenced(self): + val, errs = self.ctx.format('bar', {'num': 3}) + self.assertEqual(val, 'Foo 3') + self.assertEqual(len(errs), 0) + + def test_can_be_used_in_an_attribute(self): + val, errs = self.ctx.format('baz.attr', {'num': 3}) + self.assertEqual(val, 'Baz Attribute 3') + self.assertEqual(len(errs), 0) + + def test_can_be_used_in_a_variant(self): + val, errs = self.ctx.format('qux', {'num': 3}) + self.assertEqual(val, 'Baz Variant A 3') + self.assertEqual(len(errs), 0) + + +class TestStrings(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = { $arg } + """)) + + def test_can_be_a_string(self): + val, errs = self.ctx.format('foo', {'arg': 'Argument'}) + self.assertEqual(val, 'Argument') + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_attributes.py b/fluent.runtime/tests/format/test_attributes.py new file mode 100644 index 00000000..6e5b63fb --- /dev/null +++ b/fluent.runtime/tests/format/test_attributes.py @@ -0,0 +1,149 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentReferenceError + +from ..utils import dedent_ftl + + +class TestAttributesWithStringValues(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + .attr = Foo Attribute + bar = { foo } Bar + .attr = Bar Attribute + ref-foo = { foo.attr } + ref-bar = { bar.attr } + """)) + + def test_can_be_referenced_for_entities_with_string_values(self): + val, errs = self.ctx.format('ref-foo', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_referenced_for_entities_with_pattern_values(self): + val, errs = self.ctx.format('ref-bar', {}) + self.assertEqual(val, 'Bar Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_formatted_directly_for_entities_with_string_values(self): + val, errs = self.ctx.format('foo.attr', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_formatted_directly_for_entities_with_pattern_values(self): + val, errs = self.ctx.format('bar.attr', {}) + self.assertEqual(val, 'Bar Attribute') + self.assertEqual(len(errs), 0) + + +class TestAttributesWithSimplePatternValues(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + bar = Bar + .attr = { foo } Attribute + baz = { foo } Baz + .attr = { foo } Attribute + qux = Qux + .attr = { qux } Attribute + ref-bar = { bar.attr } + ref-baz = { baz.attr } + ref-qux = { qux.attr } + """)) + + def test_can_be_referenced_for_entities_with_string_values(self): + val, errs = self.ctx.format('ref-bar', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_formatted_directly_for_entities_with_string_values(self): + val, errs = self.ctx.format('bar.attr', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_referenced_for_entities_with_pattern_values(self): + val, errs = self.ctx.format('ref-baz', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_formatted_directly_for_entities_with_pattern_values(self): + val, errs = self.ctx.format('baz.attr', {}) + self.assertEqual(val, 'Foo Attribute') + self.assertEqual(len(errs), 0) + + def test_works_with_self_references(self): + val, errs = self.ctx.format('ref-qux', {}) + self.assertEqual(val, 'Qux Attribute') + self.assertEqual(len(errs), 0) + + def test_works_with_self_references_direct(self): + val, errs = self.ctx.format('qux.attr', {}) + self.assertEqual(val, 'Qux Attribute') + self.assertEqual(len(errs), 0) + + +class TestMissing(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + bar = Bar + .attr = Bar Attribute + baz = { foo } Baz + qux = { foo } Qux + .attr = Qux Attribute + ref-foo = { foo.missing } + ref-bar = { bar.missing } + ref-baz = { baz.missing } + ref-qux = { qux.missing } + attr-only = + .attr = Attr Only Attribute + """)) + + def test_falls_back_for_msg_with_string_value_and_no_attributes(self): + val, errs = self.ctx.format('ref-foo', {}) + self.assertEqual(val, 'Foo') + self.assertEqual(errs, + [FluentReferenceError( + 'Unknown attribute: foo.missing')]) + + def test_falls_back_for_msg_with_string_value_and_other_attributes(self): + val, errs = self.ctx.format('ref-bar', {}) + self.assertEqual(val, 'Bar') + self.assertEqual(errs, + [FluentReferenceError( + 'Unknown attribute: bar.missing')]) + + def test_falls_back_for_msg_with_pattern_value_and_no_attributes(self): + val, errs = self.ctx.format('ref-baz', {}) + self.assertEqual(val, 'Foo Baz') + self.assertEqual(errs, + [FluentReferenceError( + 'Unknown attribute: baz.missing')]) + + def test_falls_back_for_msg_with_pattern_value_and_other_attributes(self): + val, errs = self.ctx.format('ref-qux', {}) + self.assertEqual(val, 'Foo Qux') + self.assertEqual(errs, + [FluentReferenceError( + 'Unknown attribute: qux.missing')]) + + def test_attr_only_main(self): + # For reference, Javascript implementation returns null for this case. + # For Python returning `None` doesn't seem appropriate, since this will + # only blow up later if you attempt to add this to a string, so we raise + # a LookupError instead, as per entirely missing messages. + self.assertRaises(LookupError, self.ctx.format, 'attr-only', {}) + + def test_attr_only_attribute(self): + val, errs = self.ctx.format('attr-only.attr', {}) + self.assertEqual(val, 'Attr Only Attribute') + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_builtins.py b/fluent.runtime/tests/format/test_builtins.py new file mode 100644 index 00000000..f2785b0b --- /dev/null +++ b/fluent.runtime/tests/format/test_builtins.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import, unicode_literals + +import unittest +from datetime import date, datetime +from decimal import Decimal + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentReferenceError +from fluent.runtime.types import fluent_date, fluent_number + +from ..utils import dedent_ftl + + +class TestNumberBuiltin(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + implicit-call = { 123456 } + implicit-call2 = { $arg } + defaults = { NUMBER(123456) } + percent-style = { NUMBER(1.234, style: "percent") } + currency-style = { NUMBER(123456, style: "currency", currency: "USD") } + from-arg = { NUMBER($arg) } + merge-params = { NUMBER($arg, useGrouping: 0) } + """)) + + def test_implicit_call(self): + val, errs = self.ctx.format('implicit-call', {}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_implicit_call2_int(self): + val, errs = self.ctx.format('implicit-call2', {'arg': 123456}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_implicit_call2_float(self): + val, errs = self.ctx.format('implicit-call2', {'arg': 123456.0}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_implicit_call2_decimal(self): + val, errs = self.ctx.format('implicit-call2', {'arg': Decimal('123456.0')}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_defaults(self): + val, errs = self.ctx.format('defaults', {}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_percent_style(self): + val, errs = self.ctx.format('percent-style', {}) + self.assertEqual(val, "123%") + self.assertEqual(len(errs), 0) + + def test_currency_style(self): + val, errs = self.ctx.format('currency-style', {}) + self.assertEqual(val, "$123,456.00") + self.assertEqual(len(errs), 0) + + def test_from_arg_int(self): + val, errs = self.ctx.format('from-arg', {'arg': 123456}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_from_arg_float(self): + val, errs = self.ctx.format('from-arg', {'arg': 123456.0}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_from_arg_decimal(self): + val, errs = self.ctx.format('from-arg', {'arg': Decimal('123456.0')}) + self.assertEqual(val, "123,456") + self.assertEqual(len(errs), 0) + + def test_from_arg_missing(self): + val, errs = self.ctx.format('from-arg', {}) + self.assertEqual(val, "arg") + self.assertEqual(len(errs), 1) + self.assertEqual(errs, + [FluentReferenceError('Unknown external: arg')]) + + def test_partial_application(self): + number = fluent_number(123456.78, currency="USD", style="currency") + val, errs = self.ctx.format('from-arg', {'arg': number}) + self.assertEqual(val, "$123,456.78") + self.assertEqual(len(errs), 0) + + def test_merge_params(self): + number = fluent_number(123456.78, currency="USD", style="currency") + val, errs = self.ctx.format('merge-params', + {'arg': number}) + self.assertEqual(val, "$123456.78") + self.assertEqual(len(errs), 0) + + +class TestDatetimeBuiltin(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + implicit-call = { $date } + explicit-call = { DATETIME($date) } + call-with-arg = { DATETIME($date, dateStyle: "long") } + """)) + + def test_implicit_call_date(self): + val, errs = self.ctx.format('implicit-call', {'date': date(2018, 2, 1)}) + self.assertEqual(val, "Feb 1, 2018") + self.assertEqual(len(errs), 0) + + def test_implicit_call_datetime(self): + val, errs = self.ctx.format('implicit-call', {'date': datetime(2018, 2, 1, 14, 15, 16)}) + self.assertEqual(val, "Feb 1, 2018") + self.assertEqual(len(errs), 0) + + def test_explicit_call_date(self): + val, errs = self.ctx.format('explicit-call', {'date': date(2018, 2, 1)}) + self.assertEqual(val, "Feb 1, 2018") + self.assertEqual(len(errs), 0) + + def test_explicit_call_datetime(self): + val, errs = self.ctx.format('explicit-call', {'date': datetime(2018, 2, 1, 14, 15, 16)}) + self.assertEqual(val, "Feb 1, 2018") + self.assertEqual(len(errs), 0) + + def test_explicit_call_date_fluent_date(self): + val, errs = self.ctx.format('explicit-call', {'date': + fluent_date( + date(2018, 2, 1), + dateStyle='short') + }) + self.assertEqual(val, "2/1/18") + self.assertEqual(len(errs), 0) + + def test_arg(self): + val, errs = self.ctx.format('call-with-arg', {'date': date(2018, 2, 1)}) + self.assertEqual(val, "February 1, 2018") + self.assertEqual(len(errs), 0) + + def test_arg_overrides_fluent_date(self): + val, errs = self.ctx.format('call-with-arg', {'date': + fluent_date( + date(2018, 2, 1), + dateStyle='short') + }) + self.assertEqual(val, "February 1, 2018") + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_functions.py b/fluent.runtime/tests/format/test_functions.py new file mode 100644 index 00000000..fa0949cc --- /dev/null +++ b/fluent.runtime/tests/format/test_functions.py @@ -0,0 +1,159 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentReferenceError +from fluent.runtime.types import FluentNone + +from ..utils import dedent_ftl + + +class TestFunctionCalls(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False, + functions={'IDENTITY': lambda x: x}) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + .attr = Attribute + pass-nothing = { IDENTITY() } + pass-string = { IDENTITY("a") } + pass-number = { IDENTITY(1) } + pass-message = { IDENTITY(foo) } + pass-attr = { IDENTITY(foo.attr) } + pass-external = { IDENTITY($ext) } + pass-function-call = { IDENTITY(IDENTITY(1)) } + """)) + + def test_accepts_strings(self): + val, errs = self.ctx.format('pass-string', {}) + self.assertEqual(val, "a") + self.assertEqual(len(errs), 0) + + def test_accepts_numbers(self): + val, errs = self.ctx.format('pass-number', {}) + self.assertEqual(val, "1") + self.assertEqual(len(errs), 0) + + def test_accepts_entities(self): + val, errs = self.ctx.format('pass-message', {}) + self.assertEqual(val, "Foo") + self.assertEqual(len(errs), 0) + + def test_accepts_attributes(self): + val, errs = self.ctx.format('pass-attr', {}) + self.assertEqual(val, "Attribute") + self.assertEqual(len(errs), 0) + + def test_accepts_externals(self): + val, errs = self.ctx.format('pass-external', {'ext': 'Ext'}) + self.assertEqual(val, "Ext") + self.assertEqual(len(errs), 0) + + def test_accepts_function_calls(self): + val, errs = self.ctx.format('pass-function-call', {}) + self.assertEqual(val, "1") + self.assertEqual(len(errs), 0) + + def test_wrong_arity(self): + val, errs = self.ctx.format('pass-nothing', {}) + self.assertEqual(val, "IDENTITY()") + self.assertEqual(len(errs), 1) + self.assertEqual(type(errs[0]), TypeError) + + +class TestMissing(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + missing = { MISSING(1) } + """)) + + def test_falls_back_to_name_of_function(self): + val, errs = self.ctx.format("missing", {}) + self.assertEqual(val, "MISSING()") + self.assertEqual(errs, + [FluentReferenceError("Unknown function: MISSING")]) + + +class TestResolving(unittest.TestCase): + + def setUp(self): + self.args_passed = [] + + def number_processor(number): + self.args_passed.append(number) + return number + + self.ctx = FluentBundle(['en-US'], use_isolating=False, + functions={'NUMBER_PROCESSOR': + number_processor}) + + self.ctx.add_messages(dedent_ftl(""" + pass-number = { NUMBER_PROCESSOR(1) } + pass-arg = { NUMBER_PROCESSOR($arg) } + """)) + + def test_args_passed_as_numbers(self): + val, errs = self.ctx.format('pass-arg', {'arg': 1}) + self.assertEqual(val, "1") + self.assertEqual(len(errs), 0) + self.assertEqual(self.args_passed, [1]) + + def test_literals_passed_as_numbers(self): + val, errs = self.ctx.format('pass-number', {}) + self.assertEqual(val, "1") + self.assertEqual(len(errs), 0) + self.assertEqual(self.args_passed, [1]) + + +class TestKeywordArgs(unittest.TestCase): + + def setUp(self): + self.args_passed = [] + + def my_function(arg, kwarg1=None, kwarg2="default"): + self.args_passed.append((arg, kwarg1, kwarg2)) + return arg + + self.ctx = FluentBundle(['en-US'], use_isolating=False, + functions={'MYFUNC': my_function}) + self.ctx.add_messages(dedent_ftl(""" + pass-arg = { MYFUNC("a") } + pass-kwarg1 = { MYFUNC("a", kwarg1: 1) } + pass-kwarg2 = { MYFUNC("a", kwarg2: "other") } + pass-kwargs = { MYFUNC("a", kwarg1: 1, kwarg2: "other") } + pass-user-arg = { MYFUNC($arg) } + """)) + + def test_defaults(self): + val, errs = self.ctx.format('pass-arg', {}) + self.assertEqual(self.args_passed, + [("a", None, "default")]) + self.assertEqual(len(errs), 0) + + def test_pass_kwarg1(self): + val, errs = self.ctx.format('pass-kwarg1', {}) + self.assertEqual(self.args_passed, + [("a", 1, "default")]) + self.assertEqual(len(errs), 0) + + def test_pass_kwarg2(self): + val, errs = self.ctx.format('pass-kwarg2', {}) + self.assertEqual(self.args_passed, + [("a", None, "other")]) + self.assertEqual(len(errs), 0) + + def test_pass_kwargs(self): + val, errs = self.ctx.format('pass-kwargs', {}) + self.assertEqual(self.args_passed, + [("a", 1, "other")]) + self.assertEqual(len(errs), 0) + + def test_missing_arg(self): + val, errs = self.ctx.format('pass-user-arg', {}) + self.assertEqual(self.args_passed, + [(FluentNone('arg'), None, "default")]) + self.assertEqual(len(errs), 1) diff --git a/fluent.runtime/tests/format/test_isolating.py b/fluent.runtime/tests/format/test_isolating.py new file mode 100644 index 00000000..e14c1ec1 --- /dev/null +++ b/fluent.runtime/tests/format/test_isolating.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle + +from ..utils import dedent_ftl + +# Unicode bidi isolation characters. +FSI = '\u2068' +PDI = '\u2069' + + +class TestUseIsolating(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US']) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + bar = { foo } Bar + baz = { $arg } Baz + qux = { bar } { baz } + """)) + + def test_isolates_interpolated_message_references(self): + val, errs = self.ctx.format('bar', {}) + self.assertEqual(val, FSI + "Foo" + PDI + " Bar") + self.assertEqual(len(errs), 0) + + def test_isolates_interpolated_string_typed_variable_references(self): + val, errs = self.ctx.format('baz', {'arg': 'Arg'}) + self.assertEqual(val, FSI + "Arg" + PDI + " Baz") + self.assertEqual(len(errs), 0) + + def test_isolates_interpolated_number_typed_variable_references(self): + val, errs = self.ctx.format('baz', {'arg': 1}) + self.assertEqual(val, FSI + "1" + PDI + " Baz") + self.assertEqual(len(errs), 0) + + def test_isolates_complex_interpolations(self): + val, errs = self.ctx.format('qux', {'arg': 'Arg'}) + expected_bar = FSI + FSI + "Foo" + PDI + " Bar" + PDI + expected_baz = FSI + FSI + "Arg" + PDI + " Baz" + PDI + self.assertEqual(val, expected_bar + " " + expected_baz) + self.assertEqual(len(errs), 0) + + +class TestSkipIsolating(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US']) + self.ctx.add_messages(dedent_ftl(""" + -brand-short-name = Amaya + foo = { -brand-short-name } + with-arg = { $arg } + """)) + + def test_skip_isolating_chars_if_just_one_message_ref(self): + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, 'Amaya') + self.assertEqual(len(errs), 0) + + def test_skip_isolating_chars_if_just_one_placeable_arg(self): + val, errs = self.ctx.format('with-arg', {'arg': 'Arg'}) + self.assertEqual(val, 'Arg') + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_placeables.py b/fluent.runtime/tests/format/test_placeables.py new file mode 100644 index 00000000..dc61561b --- /dev/null +++ b/fluent.runtime/tests/format/test_placeables.py @@ -0,0 +1,111 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentCyclicReferenceError, FluentReferenceError + +from ..utils import dedent_ftl + + +class TestPlaceables(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + message = Message + .attr = Message Attribute + -term = Term + .attr = Term Attribute + -term2 = { + *[variant1] Term Variant 1 + [variant2] Term Variant 2 + } + + uses-message = { message } + uses-message-attr = { message.attr } + uses-term = { -term } + uses-term-variant = { -term2[variant2] } + + bad-message-ref = Text { not-a-message } + bad-message-attr-ref = Text { message.not-an-attr } + bad-term-ref = Text { -not-a-term } + + self-referencing-message = Text { self-referencing-message } + cyclic-msg1 = Text1 { cyclic-msg2 } + cyclic-msg2 = Text2 { cyclic-msg1 } + self-cyclic-message = Parent { self-cyclic-message.attr } + .attr = Attribute { self-cyclic-message } + + self-attribute-ref-ok = Parent { self-attribute-ref-ok.attr } + .attr = Attribute + self-parent-ref-ok = Parent + .attr = Attribute { self-parent-ref-ok } + """)) + + def test_placeable_message(self): + val, errs = self.ctx.format('uses-message', {}) + self.assertEqual(val, 'Message') + self.assertEqual(len(errs), 0) + + def test_placeable_message_attr(self): + val, errs = self.ctx.format('uses-message-attr', {}) + self.assertEqual(val, 'Message Attribute') + self.assertEqual(len(errs), 0) + + def test_placeable_term(self): + val, errs = self.ctx.format('uses-term', {}) + self.assertEqual(val, 'Term') + self.assertEqual(len(errs), 0) + + def test_placeable_term_variant(self): + val, errs = self.ctx.format('uses-term-variant', {}) + self.assertEqual(val, 'Term Variant 2') + self.assertEqual(len(errs), 0) + + def test_placeable_bad_message(self): + val, errs = self.ctx.format('bad-message-ref', {}) + self.assertEqual(val, 'Text not-a-message') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentReferenceError("Unknown message: not-a-message")]) + + def test_placeable_bad_message_attr(self): + val, errs = self.ctx.format('bad-message-attr-ref', {}) + self.assertEqual(val, 'Text Message') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentReferenceError("Unknown attribute: message.not-an-attr")]) + + def test_placeable_bad_term(self): + val, errs = self.ctx.format('bad-term-ref', {}) + self.assertEqual(val, 'Text -not-a-term') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentReferenceError("Unknown term: -not-a-term")]) + + def test_cycle_detection(self): + val, errs = self.ctx.format('self-referencing-message', {}) + self.assertEqual(val, 'Text ???') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentCyclicReferenceError("Cyclic reference")]) + + def test_mutual_cycle_detection(self): + val, errs = self.ctx.format('cyclic-msg1', {}) + self.assertEqual(val, 'Text1 Text2 ???') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentCyclicReferenceError("Cyclic reference")]) + + def test_allowed_self_reference(self): + val, errs = self.ctx.format('self-attribute-ref-ok', {}) + self.assertEqual(val, 'Parent Attribute') + self.assertEqual(len(errs), 0) + val, errs = self.ctx.format('self-parent-ref-ok.attr', {}) + self.assertEqual(val, 'Attribute Parent') + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_primitives.py b/fluent.runtime/tests/format/test_primitives.py new file mode 100644 index 00000000..01e86cb2 --- /dev/null +++ b/fluent.runtime/tests/format/test_primitives.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle + +from ..utils import dedent_ftl + + +class TestSimpleStringValue(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + placeable-literal = { "Foo" } Bar + placeable-message = { foo } Bar + selector-literal = { "Foo" -> + [Foo] Member 1 + *[Bar] Member 2 + } + bar = + .attr = Bar Attribute + placeable-attr = { bar.attr } + -baz = Baz + .attr = BazAttribute + selector-attr = { -baz.attr -> + [BazAttribute] Member 3 + *[other] Member 4 + } + """)) + + def test_can_be_used_as_a_value(self): + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, 'Foo') + self.assertEqual(len(errs), 0) + + def test_can_be_used_in_a_placeable(self): + val, errs = self.ctx.format('placeable-literal', {}) + self.assertEqual(val, 'Foo Bar') + self.assertEqual(len(errs), 0) + + def test_can_be_a_value_of_a_message_referenced_in_a_placeable(self): + val, errs = self.ctx.format('placeable-message', {}) + self.assertEqual(val, 'Foo Bar') + self.assertEqual(len(errs), 0) + + def test_can_be_a_selector(self): + val, errs = self.ctx.format('selector-literal', {}) + self.assertEqual(val, 'Member 1') + self.assertEqual(len(errs), 0) + + def test_can_be_used_as_an_attribute_value(self): + val, errs = self.ctx.format('bar.attr', {}) + self.assertEqual(val, 'Bar Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self): + val, errs = self.ctx.format('placeable-attr', {}) + self.assertEqual(val, 'Bar Attribute') + self.assertEqual(len(errs), 0) + + def test_can_be_a_value_of_an_attribute_used_as_a_selector(self): + val, errs = self.ctx.format('selector-attr', {}) + self.assertEqual(val, 'Member 3') + self.assertEqual(len(errs), 0) + + +class TestComplexStringValue(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + bar = { foo }Bar + + placeable-message = { bar }Baz + + baz = + .attr = { bar }BazAttribute + + -qux = Qux + .attr = { bar }QuxAttribute + + placeable-attr = { baz.attr } + + selector-attr = { -qux.attr -> + [FooBarQuxAttribute] FooBarQux + *[other] Other + } + """)) + + def test_can_be_used_as_a_value(self): + val, errs = self.ctx.format('bar', {}) + self.assertEqual(val, 'FooBar') + self.assertEqual(len(errs), 0) + + def test_can_be_value_of_a_message_referenced_in_a_placeable(self): + val, errs = self.ctx.format('placeable-message', {}) + self.assertEqual(val, 'FooBarBaz') + self.assertEqual(len(errs), 0) + + def test_can_be_used_as_an_attribute_value(self): + val, errs = self.ctx.format('baz.attr', {}) + self.assertEqual(val, 'FooBarBazAttribute') + self.assertEqual(len(errs), 0) + + def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self): + val, errs = self.ctx.format('placeable-attr', {}) + self.assertEqual(val, 'FooBarBazAttribute') + self.assertEqual(len(errs), 0) + + def test_can_be_a_value_of_an_attribute_used_as_a_selector(self): + val, errs = self.ctx.format('selector-attr', {}) + self.assertEqual(val, 'FooBarQux') + self.assertEqual(len(errs), 0) + + +class TestNumbers(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + one = { 1 } + one_point_two = { 1.2 } + select = { 1 -> + *[0] Zero + [1] One + } + """)) + + def test_int_number_used_in_placeable(self): + val, errs = self.ctx.format('one', {}) + self.assertEqual(val, '1') + self.assertEqual(len(errs), 0) + + def test_float_number_used_in_placeable(self): + val, errs = self.ctx.format('one_point_two', {}) + self.assertEqual(val, '1.2') + self.assertEqual(len(errs), 0) + + def test_can_be_used_as_a_selector(self): + val, errs = self.ctx.format('select', {}) + self.assertEqual(val, 'One') + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_select_expression.py b/fluent.runtime/tests/format/test_select_expression.py new file mode 100644 index 00000000..bc0af933 --- /dev/null +++ b/fluent.runtime/tests/format/test_select_expression.py @@ -0,0 +1,175 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentReferenceError + +from ..utils import dedent_ftl + + +class TestSelectExpressionWithStrings(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + + def test_with_a_matching_selector(self): + self.ctx.add_messages(dedent_ftl(""" + foo = { "a" -> + [a] A + *[b] B + } + """)) + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) + + def test_with_a_non_matching_selector(self): + self.ctx.add_messages(dedent_ftl(""" + foo = { "c" -> + [a] A + *[b] B + } + """)) + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, "B") + self.assertEqual(len(errs), 0) + + def test_with_a_missing_selector(self): + self.ctx.add_messages(dedent_ftl(""" + foo = { $none -> + [a] A + *[b] B + } + """)) + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, "B") + self.assertEqual(errs, + [FluentReferenceError("Unknown external: none")]) + + def test_with_argument_expression(self): + self.ctx.add_messages(dedent_ftl(""" + foo = { $arg -> + [a] A + *[b] B + } + """)) + val, errs = self.ctx.format('foo', {'arg': 'a'}) + self.assertEqual(val, "A") + + +class TestSelectExpressionWithNumbers(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = { 1 -> + *[0] A + [1] B + } + + bar = { 2 -> + *[0] A + [1] B + } + + baz = { $num -> + *[0] A + [1] B + } + + qux = { 1.0 -> + *[0] A + [1] B + } + """)) + + def test_selects_the_right_variant(self): + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, "B") + self.assertEqual(len(errs), 0) + + def test_with_a_non_matching_selector(self): + val, errs = self.ctx.format('bar', {}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) + + def test_with_a_missing_selector(self): + val, errs = self.ctx.format('baz', {}) + self.assertEqual(val, "A") + self.assertEqual(errs, + [FluentReferenceError("Unknown external: num")]) + + def test_with_argument_int(self): + val, errs = self.ctx.format('baz', {'num': 1}) + self.assertEqual(val, "B") + + def test_with_argument_float(self): + val, errs = self.ctx.format('baz', {'num': 1.0}) + self.assertEqual(val, "B") + + def test_with_float(self): + val, errs = self.ctx.format('qux', {}) + self.assertEqual(val, "B") + + +class TestSelectExpressionWithPluralCategories(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + foo = { 1 -> + [one] A + *[other] B + } + + bar = { 1 -> + [1] A + *[other] B + } + + baz = { "not a number" -> + [one] A + *[other] B + } + + qux = { $num -> + [one] A + *[other] B + } + """)) + + def test_selects_the_right_category(self): + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) + + def test_selects_exact_match(self): + val, errs = self.ctx.format('bar', {}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) + + def test_selects_default_with_invalid_selector(self): + val, errs = self.ctx.format('baz', {}) + self.assertEqual(val, "B") + self.assertEqual(len(errs), 0) + + def test_with_a_missing_selector(self): + val, errs = self.ctx.format('qux', {}) + self.assertEqual(val, "B") + self.assertEqual(errs, + [FluentReferenceError("Unknown external: num")]) + + def test_with_argument_integer(self): + val, errs = self.ctx.format('qux', {'num': 1}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) + + val, errs = self.ctx.format('qux', {'num': 2}) + self.assertEqual(val, "B") + self.assertEqual(len(errs), 0) + + def test_with_argument_float(self): + val, errs = self.ctx.format('qux', {'num': 1.0}) + self.assertEqual(val, "A") + self.assertEqual(len(errs), 0) diff --git a/fluent.runtime/tests/format/test_variants.py b/fluent.runtime/tests/format/test_variants.py new file mode 100644 index 00000000..01fdb6cd --- /dev/null +++ b/fluent.runtime/tests/format/test_variants.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle +from fluent.runtime.errors import FluentReferenceError + +from ..utils import dedent_ftl + + +class TestVariants(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + -variant = { + [a] A + *[b] B + } + foo = { -variant } + bar = { -variant[a] } + baz = { -variant[b] } + qux = { -variant[c] } + """)) + + def test_returns_the_default_variant(self): + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, 'B') + self.assertEqual(len(errs), 0) + + def test_choose_other_variant(self): + val, errs = self.ctx.format('bar', {}) + self.assertEqual(val, 'A') + self.assertEqual(len(errs), 0) + + def test_choose_default_variant(self): + val, errs = self.ctx.format('baz', {}) + self.assertEqual(val, 'B') + self.assertEqual(len(errs), 0) + + def test_choose_missing_variant(self): + val, errs = self.ctx.format('qux', {}) + self.assertEqual(val, 'B') + self.assertEqual(len(errs), 1) + self.assertEqual( + errs, + [FluentReferenceError("Unknown variant: c")]) diff --git a/fluent.runtime/tests/test_bomb.py b/fluent.runtime/tests/test_bomb.py new file mode 100644 index 00000000..889cb5a0 --- /dev/null +++ b/fluent.runtime/tests/test_bomb.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle + +from .utils import dedent_ftl + + +class TestBillionLaughs(unittest.TestCase): + + def setUp(self): + self.ctx = FluentBundle(['en-US'], use_isolating=False) + self.ctx.add_messages(dedent_ftl(""" + lol0 = 01234567890123456789012345678901234567890123456789 + lol1 = {lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0} + lol2 = {lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1} + lol3 = {lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2} + lol4 = {lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3} + lolz = {lol4} + + elol0 = { "" } + elol1 = {elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0} + elol2 = {elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1} + elol3 = {elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2} + elol4 = {elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3} + elol5 = {elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4} + elol6 = {elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5} + emptylolz = {elol6} + + """)) + + def test_max_length_protection(self): + val, errs = self.ctx.format('lolz') + self.assertEqual(val, ('0123456789' * 1000)[0:2500]) + self.assertNotEqual(len(errs), 0) + + def test_max_expansions_protection(self): + # Without protection, emptylolz will take a really long time to + # evaluate, although it generates an empty message. + val, errs = self.ctx.format('emptylolz') + self.assertEqual(val, '???') + self.assertEqual(len(errs), 1) diff --git a/fluent.runtime/tests/test_bundle.py b/fluent.runtime/tests/test_bundle.py new file mode 100644 index 00000000..d11a4819 --- /dev/null +++ b/fluent.runtime/tests/test_bundle.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import unittest + +from fluent.runtime import FluentBundle + +from .utils import dedent_ftl + + +class TestFluentBundle(unittest.TestCase): + def setUp(self): + self.ctx = FluentBundle(['en-US']) + + def test_add_messages(self): + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + bar = Bar + -baz = Baz + """)) + self.assertIn('foo', self.ctx._messages_and_terms) + self.assertIn('bar', self.ctx._messages_and_terms) + self.assertIn('-baz', self.ctx._messages_and_terms) + + def test_has_message(self): + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + """)) + + self.assertTrue(self.ctx.has_message('foo')) + self.assertFalse(self.ctx.has_message('bar')) + + def test_has_message_for_term(self): + self.ctx.add_messages(dedent_ftl(""" + -foo = Foo + """)) + + self.assertFalse(self.ctx.has_message('-foo')) + + def test_has_message_with_attribute(self): + self.ctx.add_messages(dedent_ftl(""" + foo = Foo + .attr = Foo Attribute + """)) + + self.assertTrue(self.ctx.has_message('foo')) + self.assertFalse(self.ctx.has_message('foo.attr')) + self.assertFalse(self.ctx.has_message('foo.other-attribute')) + + def test_plural_form_english_ints(self): + ctx = FluentBundle(['en-US']) + self.assertEqual(ctx._plural_form(0), + 'other') + self.assertEqual(ctx._plural_form(1), + 'one') + self.assertEqual(ctx._plural_form(2), + 'other') + + def test_plural_form_english_floats(self): + ctx = FluentBundle(['en-US']) + self.assertEqual(ctx._plural_form(0.0), + 'other') + self.assertEqual(ctx._plural_form(1.0), + 'one') + self.assertEqual(ctx._plural_form(2.0), + 'other') + self.assertEqual(ctx._plural_form(0.5), + 'other') + + def test_plural_form_french(self): + # Just spot check one other, to ensure that we + # are not getting the EN locale by accident or + ctx = FluentBundle(['fr']) + self.assertEqual(ctx._plural_form(0), + 'one') + self.assertEqual(ctx._plural_form(1), + 'one') + self.assertEqual(ctx._plural_form(2), + 'other') + + def test_format_args(self): + self.ctx.add_messages('foo = Foo') + val, errs = self.ctx.format('foo') + self.assertEqual(val, 'Foo') + + val, errs = self.ctx.format('foo', {}) + self.assertEqual(val, 'Foo') + + def test_format_missing(self): + self.assertRaises(LookupError, + self.ctx.format, + 'a-missing-message') + + def test_format_term(self): + self.ctx.add_messages(dedent_ftl(""" + -foo = Foo + """)) + self.assertRaises(LookupError, + self.ctx.format, + '-foo') diff --git a/fluent.runtime/tests/test_types.py b/fluent.runtime/tests/test_types.py new file mode 100644 index 00000000..8e8a2dd4 --- /dev/null +++ b/fluent.runtime/tests/test_types.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import unittest +import warnings +from datetime import date, datetime +from decimal import Decimal + +import pytz +from babel import Locale + +from fluent.runtime.types import FluentDateType, FluentNumber, fluent_date, fluent_number + + +class TestFluentNumber(unittest.TestCase): + + locale = Locale.parse('en_US') + + def setUp(self): + self.cur_pos = fluent_number(123456.78123, + currency='USD', + style='currency') + self.cur_neg = fluent_number(-123456.78123, + currency='USD', + style='currency') + + def test_int(self): + i = fluent_number(1) + self.assertTrue(isinstance(i, int)) + self.assertTrue(isinstance(i, FluentNumber)) + self.assertEqual(i + 1, 2) + + def test_float(self): + f = fluent_number(1.1) + self.assertTrue(isinstance(f, float)) + self.assertTrue(isinstance(f, FluentNumber)) + self.assertEqual(f + 1, 2.1) + + def test_decimal(self): + d = Decimal('1.1') + self.assertTrue(isinstance(fluent_number(d), Decimal)) + self.assertTrue(isinstance(fluent_number(d), FluentNumber)) + self.assertEqual(d + 1, Decimal('2.1')) + + def test_disallow_nonexistant_options(self): + self.assertRaises( + TypeError, + fluent_number, + 1, + not_a_real_option=True, + ) + + def test_style_validation(self): + self.assertRaises(ValueError, + fluent_number, + 1, + style='xyz') + + def test_use_grouping(self): + f1 = fluent_number(123456.78, useGrouping=True) + f2 = fluent_number(123456.78, useGrouping=False) + self.assertEqual(f1.format(self.locale), "123,456.78") + self.assertEqual(f2.format(self.locale), "123456.78") + # ensure we didn't mutate anything when we created the new + # NumberPattern: + self.assertEqual(f1.format(self.locale), "123,456.78") + + def test_use_grouping_decimal(self): + d = Decimal('123456.78') + f1 = fluent_number(d, useGrouping=True) + f2 = fluent_number(d, useGrouping=False) + self.assertEqual(f1.format(self.locale), "123,456.78") + self.assertEqual(f2.format(self.locale), "123456.78") + + def test_minimum_integer_digits(self): + f = fluent_number(1.23, minimumIntegerDigits=3) + self.assertEqual(f.format(self.locale), "001.23") + + def test_minimum_integer_digits_decimal(self): + f = fluent_number(Decimal('1.23'), minimumIntegerDigits=3) + self.assertEqual(f.format(self.locale), "001.23") + + def test_minimum_fraction_digits(self): + f = fluent_number(1.2, minimumFractionDigits=3) + self.assertEqual(f.format(self.locale), "1.200") + + def test_maximum_fraction_digits(self): + f1 = fluent_number(1.23456) + self.assertEqual(f1.format(self.locale), "1.235") + f2 = fluent_number(1.23456, maximumFractionDigits=5) + self.assertEqual(f2.format(self.locale), "1.23456") + + def test_minimum_significant_digits(self): + f1 = fluent_number(123, minimumSignificantDigits=5) + self.assertEqual(f1.format(self.locale), "123.00") + f2 = fluent_number(12.3, minimumSignificantDigits=5) + self.assertEqual(f2.format(self.locale), "12.300") + + def test_maximum_significant_digits(self): + f1 = fluent_number(123456, maximumSignificantDigits=3) + self.assertEqual(f1.format(self.locale), "123,000") + f2 = fluent_number(12.3456, maximumSignificantDigits=3) + self.assertEqual(f2.format(self.locale), "12.3") + f3 = fluent_number(12, maximumSignificantDigits=5) + self.assertEqual(f3.format(self.locale), "12") + + def test_currency(self): + # This test the default currencyDisplay value + self.assertEqual(self.cur_pos.format(self.locale), "$123,456.78") + + def test_currency_display_validation(self): + self.assertRaises(ValueError, + fluent_number, + 1234, + currencyDisplay="junk") + + def test_currency_display_symbol(self): + cur_pos_sym = fluent_number(self.cur_pos, currencyDisplay="symbol") + cur_neg_sym = fluent_number(self.cur_neg, currencyDisplay="symbol") + self.assertEqual(cur_pos_sym.format(self.locale), "$123,456.78") + self.assertEqual(cur_neg_sym.format(self.locale), "-$123,456.78") + + def test_currency_display_code(self): + # Outputs here were determined by comparing with Javascrpt + # Intl.NumberFormat in Firefox. + cur_pos_code = fluent_number(self.cur_pos, currencyDisplay="code") + cur_neg_code = fluent_number(self.cur_neg, currencyDisplay="code") + self.assertEqual(cur_pos_code.format(self.locale), "USD123,456.78") + self.assertEqual(cur_neg_code.format(self.locale), "-USD123,456.78") + + @unittest.skip("Babel doesn't provide support for this yet") + def test_currency_display_name(self): + cur_pos_name = fluent_number(self.cur_pos, currencyDisplay="name") + cur_neg_name = fluent_number(self.cur_neg, currencyDisplay="name") + self.assertEqual(cur_pos_name.format(self.locale), "123,456.78 US dollars") + self.assertEqual(cur_neg_name.format(self.locale), "-123,456.78 US dollars") + + # Some others locales: + hr_BA = Locale.parse('hr_BA') + self.assertEqual(cur_pos_name.format(hr_BA), + "123.456,78 američkih dolara") + es_GT = Locale.parse('es_GT') + self.assertEqual(cur_pos_name.format(es_GT), + "dólares estadounidenses 123,456.78") + + def test_copy_attributes(self): + f1 = fluent_number(123456.78, useGrouping=False) + self.assertEqual(f1.options.useGrouping, False) + + # Check we didn't mutate anything + self.assertIs(FluentNumber.default_number_format_options.useGrouping, True) + + f2 = fluent_number(f1, style="percent") + self.assertEqual(f2.options.style, "percent") + + # Check we copied + self.assertEqual(f2.options.useGrouping, False) + + # and didn't mutate anything + self.assertEqual(f1.options.style, "decimal") + self.assertEqual(FluentNumber.default_number_format_options.style, "decimal") + + +class TestFluentDate(unittest.TestCase): + + locale = Locale.parse('en_US') + + def setUp(self): + self.a_date = date(2018, 2, 1) + self.a_datetime = datetime(2018, 2, 1, 14, 15, 16, 123456, + tzinfo=pytz.UTC) + + def test_date(self): + fd = fluent_date(self.a_date) + self.assertTrue(isinstance(fd, date)) + self.assertTrue(isinstance(fd, FluentDateType)) + self.assertEqual(fd.year, self.a_date.year) + self.assertEqual(fd.month, self.a_date.month) + self.assertEqual(fd.day, self.a_date.day) + + def test_datetime(self): + fd = fluent_date(self.a_datetime) + self.assertTrue(isinstance(fd, datetime)) + self.assertTrue(isinstance(fd, FluentDateType)) + self.assertEqual(fd.year, self.a_datetime.year) + self.assertEqual(fd.month, self.a_datetime.month) + self.assertEqual(fd.day, self.a_datetime.day) + self.assertEqual(fd.hour, self.a_datetime.hour) + self.assertEqual(fd.minute, self.a_datetime.minute) + self.assertEqual(fd.second, self.a_datetime.second) + self.assertEqual(fd.microsecond, self.a_datetime.microsecond) + self.assertEqual(fd.tzinfo, self.a_datetime.tzinfo) + + def test_format_defaults(self): + fd = fluent_date(self.a_date) + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_GB), '1 Feb 2018') + self.assertEqual(fd.format(en_US), 'Feb 1, 2018') + + def test_dateStyle_date(self): + fd = fluent_date(self.a_date, dateStyle='long') + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_GB), '1 February 2018') + self.assertEqual(fd.format(en_US), 'February 1, 2018') + + def test_dateStyle_datetime(self): + fd = fluent_date(self.a_datetime, dateStyle='long') + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_GB), '1 February 2018') + self.assertEqual(fd.format(en_US), 'February 1, 2018') + + def test_timeStyle_datetime(self): + fd = fluent_date(self.a_datetime, timeStyle='short') + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_US), '2:15 PM') + self.assertEqual(fd.format(en_GB), '14:15') + + def test_dateStyle_and_timeStyle_datetime(self): + fd = fluent_date(self.a_datetime, timeStyle='short', dateStyle='short') + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_US), '2/1/18, 2:15 PM') + self.assertEqual(fd.format(en_GB), '01/02/2018, 14:15') + + def test_validate_dateStyle(self): + self.assertRaises(ValueError, + fluent_date, + self.a_date, + dateStyle="nothing") + + def test_validate_timeStyle(self): + self.assertRaises(ValueError, + fluent_date, + self.a_datetime, + timeStyle="nothing") + + def test_timeZone(self): + en_GB = Locale.parse('en_GB') + LondonTZ = pytz.timezone('Europe/London') + + # 1st July is a date in British Summer Time + + # datetime object with tzinfo set to BST + dt1 = datetime(2018, 7, 1, 23, 30, 0, tzinfo=pytz.UTC).astimezone(LondonTZ) + fd1 = fluent_date(dt1, dateStyle='short', timeStyle='short') + self.assertEqual(fd1.format(en_GB), '02/07/2018, 00:30') + fd1b = fluent_date(dt1, dateStyle='full', timeStyle='full') + self.assertEqual(fd1b.format(en_GB), 'Monday, 2 July 2018 at 00:30:00 British Summer Time') + fd1c = fluent_date(dt1, dateStyle='short') + self.assertEqual(fd1c.format(en_GB), '02/07/2018') + fd1d = fluent_date(dt1, timeStyle='short') + self.assertEqual(fd1d.format(en_GB), '00:30') + + # datetime object with no TZ, TZ passed in to fluent_date + dt2 = datetime(2018, 7, 1, 23, 30, 0) # Assumed UTC + fd2 = fluent_date(dt2, dateStyle='short', timeStyle='short', + timeZone='Europe/London') + self.assertEqual(fd2.format(en_GB), '02/07/2018, 00:30') + fd2b = fluent_date(dt2, dateStyle='full', timeStyle='full', + timeZone='Europe/London') + self.assertEqual(fd2b.format(en_GB), 'Monday, 2 July 2018 at 00:30:00 British Summer Time') + fd2c = fluent_date(dt2, dateStyle='short', + timeZone='Europe/London') + self.assertEqual(fd2c.format(en_GB), '02/07/2018') + fd2d = fluent_date(dt1, timeStyle='short', + timeZone='Europe/London') + self.assertEqual(fd2d.format(en_GB), '00:30') + + def test_allow_unsupported_options(self): + # We are just checking that these don't raise exceptions + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fluent_date(self.a_date, + hour12=True, + weekday="narrow", + era="narrow", + year="numeric", + month="numeric", + day="numeric", + hour="numeric", + minute="numeric", + second="numeric", + timeZoneName="short", + ) + + def test_disallow_nonexistant_options(self): + self.assertRaises( + TypeError, + fluent_date, + self.a_date, + not_a_real_option=True, + ) + + def test_dont_wrap_unnecessarily(self): + f1 = fluent_date(self.a_date) + f2 = fluent_date(f1) + self.assertIs(f1, f2) + + def test_copy_attributes(self): + f1 = fluent_date(self.a_date, dateStyle='long', hour12=False) + self.assertEqual(f1.options.dateStyle, 'long') + + f2 = fluent_date(f1, hour12=False) + + # Check we copied other attributes: + self.assertEqual(f2.options.dateStyle, "long") + self.assertEqual(f2.options.hour12, False) + + # Check we can override + f3 = fluent_date(f2, dateStyle="full") + self.assertEqual(f3.options.dateStyle, "full") + + # and didn't mutate anything + self.assertEqual(f1.options.dateStyle, "long") + self.assertEqual(f2.options.dateStyle, "long") diff --git a/fluent.runtime/tests/utils.py b/fluent.runtime/tests/utils.py new file mode 100644 index 00000000..5bb38076 --- /dev/null +++ b/fluent.runtime/tests/utils.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +import textwrap + + +def dedent_ftl(text): + return textwrap.dedent("{}\n".format(text.rstrip())) diff --git a/fluent.runtime/tox.ini b/fluent.runtime/tox.ini new file mode 100644 index 00000000..960d76d9 --- /dev/null +++ b/fluent.runtime/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = py27, py35, py36, pypy, pypy3 +skipsdist=True + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + fluent>=0.9,<0.10 + six + attrs + Babel + py27: singledispatch + pypy: singledispatch +commands = ./runtests.py diff --git a/CHANGELOG.md b/fluent.syntax/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to fluent.syntax/CHANGELOG.md diff --git a/fluent.syntax/fluent/__init__.py b/fluent.syntax/fluent/__init__.py new file mode 100644 index 00000000..69e3be50 --- /dev/null +++ b/fluent.syntax/fluent/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/fluent/syntax/__init__.py b/fluent.syntax/fluent/syntax/__init__.py similarity index 100% rename from fluent/syntax/__init__.py rename to fluent.syntax/fluent/syntax/__init__.py diff --git a/fluent/syntax/ast.py b/fluent.syntax/fluent/syntax/ast.py similarity index 100% rename from fluent/syntax/ast.py rename to fluent.syntax/fluent/syntax/ast.py diff --git a/fluent/syntax/errors.py b/fluent.syntax/fluent/syntax/errors.py similarity index 100% rename from fluent/syntax/errors.py rename to fluent.syntax/fluent/syntax/errors.py diff --git a/fluent/syntax/parser.py b/fluent.syntax/fluent/syntax/parser.py similarity index 100% rename from fluent/syntax/parser.py rename to fluent.syntax/fluent/syntax/parser.py diff --git a/fluent/syntax/serializer.py b/fluent.syntax/fluent/syntax/serializer.py similarity index 100% rename from fluent/syntax/serializer.py rename to fluent.syntax/fluent/syntax/serializer.py diff --git a/fluent/syntax/stream.py b/fluent.syntax/fluent/syntax/stream.py similarity index 100% rename from fluent/syntax/stream.py rename to fluent.syntax/fluent/syntax/stream.py diff --git a/fluent.syntax/runtests.py b/fluent.syntax/runtests.py new file mode 120000 index 00000000..b05a571a --- /dev/null +++ b/fluent.syntax/runtests.py @@ -0,0 +1 @@ +../fluent.runtime/runtests.py \ No newline at end of file diff --git a/fluent.syntax/setup.cfg b/fluent.syntax/setup.cfg new file mode 100644 index 00000000..02d416e8 --- /dev/null +++ b/fluent.syntax/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +universal=1 + +[flake8] +exclude=.tox +max-line-length=120 + +[isort] +line_length=120 +skip_glob=.tox +not_skip=__init__.py diff --git a/setup.py b/fluent.syntax/setup.py similarity index 86% rename from setup.py rename to fluent.syntax/setup.py index 21dfd0fb..0e968c06 100644 --- a/setup.py +++ b/fluent.syntax/setup.py @@ -1,10 +1,10 @@ #!/usr/bin/env python - from setuptools import setup -setup(name='fluent', +setup(name='fluent.syntax', version='0.10.0', description='Localization library for expressive translations.', + long_description='See https://github.com/projectfluent/python-fluent/ for more info.', author='Mozilla', author_email='l10n-drivers@mozilla.org', license='APL 2', diff --git a/tests/syntax/fixtures_reference/eof_empty.ftl b/fluent.syntax/tests/__init__.py similarity index 100% rename from tests/syntax/fixtures_reference/eof_empty.ftl rename to fluent.syntax/tests/__init__.py diff --git a/tests/syntax/README.md b/fluent.syntax/tests/syntax/README.md similarity index 100% rename from tests/syntax/README.md rename to fluent.syntax/tests/syntax/README.md diff --git a/tests/syntax/__init__.py b/fluent.syntax/tests/syntax/__init__.py similarity index 70% rename from tests/syntax/__init__.py rename to fluent.syntax/tests/syntax/__init__.py index 46193b0d..5bb38076 100644 --- a/tests/syntax/__init__.py +++ b/fluent.syntax/tests/syntax/__init__.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import textwrap diff --git a/tests/syntax/fixtures_behavior/attribute_expression_with_wrong_attr.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_expression_with_wrong_attr.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_expression_with_wrong_attr.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_expression_with_wrong_attr.ftl diff --git a/tests/syntax/fixtures_behavior/attribute_of_private_as_placeable.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_of_private_as_placeable.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_of_private_as_placeable.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_of_private_as_placeable.ftl diff --git a/tests/syntax/fixtures_behavior/attribute_of_public_as_selector.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_of_public_as_selector.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_of_public_as_selector.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_of_public_as_selector.ftl diff --git a/tests/syntax/fixtures_behavior/attribute_starts_from_nl.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_starts_from_nl.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_starts_from_nl.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_starts_from_nl.ftl diff --git a/tests/syntax/fixtures_behavior/attribute_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_behavior/attribute_without_equal_sign.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/attribute_without_equal_sign.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/attribute_without_equal_sign.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/attribute_without_equal_sign.ftl diff --git a/tests/syntax/fixtures_behavior/broken_number.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/broken_number.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/broken_number.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/broken_number.ftl diff --git a/tests/syntax/fixtures_behavior/call_expression_with_bad_id.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_bad_id.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/call_expression_with_bad_id.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_bad_id.ftl diff --git a/tests/syntax/fixtures_behavior/call_expression_with_trailing_comma.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_trailing_comma.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/call_expression_with_trailing_comma.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_trailing_comma.ftl diff --git a/tests/syntax/fixtures_behavior/call_expression_with_wrong_kwarg_name.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_wrong_kwarg_name.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/call_expression_with_wrong_kwarg_name.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_wrong_kwarg_name.ftl diff --git a/tests/syntax/fixtures_behavior/call_expression_with_wrong_value_type.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_wrong_value_type.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/call_expression_with_wrong_value_type.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/call_expression_with_wrong_value_type.ftl diff --git a/tests/syntax/fixtures_behavior/comment_continues_with_one_slash.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/comment_continues_with_one_slash.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/comment_continues_with_one_slash.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/comment_continues_with_one_slash.ftl diff --git a/tests/syntax/fixtures_behavior/comment_with_eof.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/comment_with_eof.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/comment_with_eof.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/comment_with_eof.ftl diff --git a/fluent.syntax/tests/syntax/fixtures_behavior/empty_resource.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/empty_resource.ftl new file mode 100644 index 00000000..e69de29b diff --git a/tests/syntax/fixtures_behavior/empty_resource_with_ws.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/empty_resource_with_ws.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/empty_resource_with_ws.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/empty_resource_with_ws.ftl diff --git a/tests/syntax/fixtures_behavior/entry_start_with_one_slash.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/entry_start_with_one_slash.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/entry_start_with_one_slash.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/entry_start_with_one_slash.ftl diff --git a/tests/syntax/fixtures_behavior/escape_sequences.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/escape_sequences.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/escape_sequences.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/escape_sequences.ftl diff --git a/tests/syntax/fixtures_behavior/indent.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/indent.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/indent.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/indent.ftl diff --git a/tests/syntax/fixtures_behavior/leading_empty_lines.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/leading_empty_lines.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/leading_empty_lines.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/leading_empty_lines.ftl diff --git a/tests/syntax/fixtures_behavior/leading_empty_lines_with_ws.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/leading_empty_lines_with_ws.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/leading_empty_lines_with_ws.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/leading_empty_lines_with_ws.ftl diff --git a/tests/syntax/fixtures_behavior/message_reference_as_selector.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/message_reference_as_selector.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/message_reference_as_selector.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/message_reference_as_selector.ftl diff --git a/tests/syntax/fixtures_behavior/multiline_string.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/multiline_string.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/multiline_string.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/multiline_string.ftl diff --git a/tests/syntax/fixtures_behavior/multiline_with_non_empty_first_line.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/multiline_with_non_empty_first_line.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/multiline_with_non_empty_first_line.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/multiline_with_non_empty_first_line.ftl diff --git a/tests/syntax/fixtures_behavior/multiline_with_placeables.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/multiline_with_placeables.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/multiline_with_placeables.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/multiline_with_placeables.ftl diff --git a/tests/syntax/fixtures_behavior/non_id_attribute_name.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/non_id_attribute_name.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/non_id_attribute_name.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/non_id_attribute_name.ftl diff --git a/tests/syntax/fixtures_behavior/placeable_at_line_extremes.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/placeable_at_line_extremes.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/placeable_at_line_extremes.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/placeable_at_line_extremes.ftl diff --git a/tests/syntax/fixtures_behavior/placeable_in_placeable.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/placeable_in_placeable.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/placeable_in_placeable.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/placeable_in_placeable.ftl diff --git a/tests/syntax/fixtures_behavior/placeable_without_close_bracket.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/placeable_without_close_bracket.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/placeable_without_close_bracket.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/placeable_without_close_bracket.ftl diff --git a/tests/syntax/fixtures_behavior/second_attribute_starts_from_nl.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/second_attribute_starts_from_nl.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/second_attribute_starts_from_nl.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/second_attribute_starts_from_nl.ftl diff --git a/tests/syntax/fixtures_behavior/section_starts_with_one_bracket.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/section_starts_with_one_bracket.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/section_starts_with_one_bracket.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/section_starts_with_one_bracket.ftl diff --git a/tests/syntax/fixtures_behavior/section_with_nl_in_the_middle.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/section_with_nl_in_the_middle.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/section_with_nl_in_the_middle.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/section_with_nl_in_the_middle.ftl diff --git a/tests/syntax/fixtures_behavior/section_with_no_nl_after_it.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/section_with_no_nl_after_it.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/section_with_no_nl_after_it.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/section_with_no_nl_after_it.ftl diff --git a/tests/syntax/fixtures_behavior/section_with_one_bracket_at_the_end.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/section_with_one_bracket_at_the_end.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/section_with_one_bracket_at_the_end.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/section_with_one_bracket_at_the_end.ftl diff --git a/tests/syntax/fixtures_behavior/select_expression_with_two_selectors.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/select_expression_with_two_selectors.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/select_expression_with_two_selectors.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/select_expression_with_two_selectors.ftl diff --git a/tests/syntax/fixtures_behavior/select_expression_without_arrow.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/select_expression_without_arrow.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/select_expression_without_arrow.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/select_expression_without_arrow.ftl diff --git a/tests/syntax/fixtures_behavior/select_expression_without_variants.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/select_expression_without_variants.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/select_expression_without_variants.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/select_expression_without_variants.ftl diff --git a/tests/syntax/fixtures_behavior/selector_expression_ends_abruptly.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/selector_expression_ends_abruptly.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/selector_expression_ends_abruptly.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/selector_expression_ends_abruptly.ftl diff --git a/tests/syntax/fixtures_behavior/simple_message.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/simple_message.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/simple_message.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/simple_message.ftl diff --git a/tests/syntax/fixtures_behavior/single_char_id.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/single_char_id.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/single_char_id.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/single_char_id.ftl diff --git a/tests/syntax/fixtures_behavior/standalone_identifier.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/standalone_identifier.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/standalone_identifier.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/standalone_identifier.ftl diff --git a/tests/syntax/fixtures_behavior/term.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/term.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/term.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/term.ftl diff --git a/tests/syntax/fixtures_behavior/unclosed_empty_placeable_error.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/unclosed_empty_placeable_error.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/unclosed_empty_placeable_error.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/unclosed_empty_placeable_error.ftl diff --git a/tests/syntax/fixtures_behavior/unknown_entry_start.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/unknown_entry_start.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/unknown_entry_start.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/unknown_entry_start.ftl diff --git a/tests/syntax/fixtures_behavior/variant_ends_abruptly.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_ends_abruptly.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_ends_abruptly.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_ends_abruptly.ftl diff --git a/tests/syntax/fixtures_behavior/variant_expression_as_placeable.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_as_placeable.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_expression_as_placeable.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_as_placeable.ftl diff --git a/tests/syntax/fixtures_behavior/variant_expression_as_selector.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_as_selector.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_expression_as_selector.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_as_selector.ftl diff --git a/tests/syntax/fixtures_behavior/variant_expression_empty_key.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_empty_key.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_expression_empty_key.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_expression_empty_key.ftl diff --git a/tests/syntax/fixtures_behavior/variant_lists.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_lists.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_lists.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_lists.ftl diff --git a/tests/syntax/fixtures_behavior/variant_starts_from_nl.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_starts_from_nl.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_starts_from_nl.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_starts_from_nl.ftl diff --git a/tests/syntax/fixtures_behavior/variant_with_digit_key.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_with_digit_key.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_with_digit_key.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_with_digit_key.ftl diff --git a/tests/syntax/fixtures_behavior/variant_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_behavior/variant_with_leading_space_in_name.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_with_leading_space_in_name.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_with_leading_space_in_name.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_with_leading_space_in_name.ftl diff --git a/tests/syntax/fixtures_behavior/variant_with_symbol_with_space.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variant_with_symbol_with_space.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variant_with_symbol_with_space.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variant_with_symbol_with_space.ftl diff --git a/tests/syntax/fixtures_behavior/variants_with_two_defaults.ftl b/fluent.syntax/tests/syntax/fixtures_behavior/variants_with_two_defaults.ftl similarity index 100% rename from tests/syntax/fixtures_behavior/variants_with_two_defaults.ftl rename to fluent.syntax/tests/syntax/fixtures_behavior/variants_with_two_defaults.ftl diff --git a/tests/syntax/fixtures_reference/any_char.ftl b/fluent.syntax/tests/syntax/fixtures_reference/any_char.ftl similarity index 100% rename from tests/syntax/fixtures_reference/any_char.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/any_char.ftl diff --git a/tests/syntax/fixtures_reference/any_char.json b/fluent.syntax/tests/syntax/fixtures_reference/any_char.json similarity index 100% rename from tests/syntax/fixtures_reference/any_char.json rename to fluent.syntax/tests/syntax/fixtures_reference/any_char.json diff --git a/tests/syntax/fixtures_reference/astral.ftl b/fluent.syntax/tests/syntax/fixtures_reference/astral.ftl similarity index 100% rename from tests/syntax/fixtures_reference/astral.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/astral.ftl diff --git a/tests/syntax/fixtures_reference/astral.json b/fluent.syntax/tests/syntax/fixtures_reference/astral.json similarity index 100% rename from tests/syntax/fixtures_reference/astral.json rename to fluent.syntax/tests/syntax/fixtures_reference/astral.json diff --git a/tests/syntax/fixtures_reference/call_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/call_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl diff --git a/tests/syntax/fixtures_reference/call_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/call_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json diff --git a/tests/syntax/fixtures_reference/callee_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/callee_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/callee_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/callee_expressions.ftl diff --git a/tests/syntax/fixtures_reference/callee_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/callee_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/callee_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/callee_expressions.json diff --git a/tests/syntax/fixtures_reference/comments.ftl b/fluent.syntax/tests/syntax/fixtures_reference/comments.ftl similarity index 100% rename from tests/syntax/fixtures_reference/comments.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/comments.ftl diff --git a/tests/syntax/fixtures_reference/comments.json b/fluent.syntax/tests/syntax/fixtures_reference/comments.json similarity index 100% rename from tests/syntax/fixtures_reference/comments.json rename to fluent.syntax/tests/syntax/fixtures_reference/comments.json diff --git a/tests/syntax/fixtures_reference/cr.ftl b/fluent.syntax/tests/syntax/fixtures_reference/cr.ftl similarity index 100% rename from tests/syntax/fixtures_reference/cr.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/cr.ftl diff --git a/tests/syntax/fixtures_reference/cr.json b/fluent.syntax/tests/syntax/fixtures_reference/cr.json similarity index 100% rename from tests/syntax/fixtures_reference/cr.json rename to fluent.syntax/tests/syntax/fixtures_reference/cr.json diff --git a/tests/syntax/fixtures_reference/crlf.ftl b/fluent.syntax/tests/syntax/fixtures_reference/crlf.ftl similarity index 100% rename from tests/syntax/fixtures_reference/crlf.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/crlf.ftl diff --git a/tests/syntax/fixtures_reference/crlf.json b/fluent.syntax/tests/syntax/fixtures_reference/crlf.json similarity index 100% rename from tests/syntax/fixtures_reference/crlf.json rename to fluent.syntax/tests/syntax/fixtures_reference/crlf.json diff --git a/tests/syntax/fixtures_reference/eof_comment.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_comment.ftl similarity index 100% rename from tests/syntax/fixtures_reference/eof_comment.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/eof_comment.ftl diff --git a/tests/syntax/fixtures_reference/eof_comment.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_comment.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_comment.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_comment.json diff --git a/fluent.syntax/tests/syntax/fixtures_reference/eof_empty.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_empty.ftl new file mode 100644 index 00000000..e69de29b diff --git a/tests/syntax/fixtures_reference/eof_empty.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_empty.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_empty.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_empty.json diff --git a/tests/syntax/fixtures_reference/eof_id.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_id.ftl similarity index 100% rename from tests/syntax/fixtures_reference/eof_id.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/eof_id.ftl diff --git a/tests/syntax/fixtures_reference/eof_id.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_id.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_id.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_id.json diff --git a/tests/syntax/fixtures_reference/eof_id_equals.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.ftl similarity index 100% rename from tests/syntax/fixtures_reference/eof_id_equals.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.ftl diff --git a/tests/syntax/fixtures_reference/eof_id_equals.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_id_equals.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.json diff --git a/tests/syntax/fixtures_reference/eof_junk.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_junk.ftl similarity index 100% rename from tests/syntax/fixtures_reference/eof_junk.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/eof_junk.ftl diff --git a/tests/syntax/fixtures_reference/eof_junk.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_junk.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_junk.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_junk.json diff --git a/tests/syntax/fixtures_reference/eof_value.ftl b/fluent.syntax/tests/syntax/fixtures_reference/eof_value.ftl similarity index 100% rename from tests/syntax/fixtures_reference/eof_value.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/eof_value.ftl diff --git a/tests/syntax/fixtures_reference/eof_value.json b/fluent.syntax/tests/syntax/fixtures_reference/eof_value.json similarity index 100% rename from tests/syntax/fixtures_reference/eof_value.json rename to fluent.syntax/tests/syntax/fixtures_reference/eof_value.json diff --git a/tests/syntax/fixtures_reference/escaped_characters.ftl b/fluent.syntax/tests/syntax/fixtures_reference/escaped_characters.ftl similarity index 100% rename from tests/syntax/fixtures_reference/escaped_characters.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/escaped_characters.ftl diff --git a/tests/syntax/fixtures_reference/escaped_characters.json b/fluent.syntax/tests/syntax/fixtures_reference/escaped_characters.json similarity index 100% rename from tests/syntax/fixtures_reference/escaped_characters.json rename to fluent.syntax/tests/syntax/fixtures_reference/escaped_characters.json diff --git a/tests/syntax/fixtures_reference/junk.ftl b/fluent.syntax/tests/syntax/fixtures_reference/junk.ftl similarity index 100% rename from tests/syntax/fixtures_reference/junk.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/junk.ftl diff --git a/tests/syntax/fixtures_reference/junk.json b/fluent.syntax/tests/syntax/fixtures_reference/junk.json similarity index 100% rename from tests/syntax/fixtures_reference/junk.json rename to fluent.syntax/tests/syntax/fixtures_reference/junk.json diff --git a/tests/syntax/fixtures_reference/leading_dots.ftl b/fluent.syntax/tests/syntax/fixtures_reference/leading_dots.ftl similarity index 100% rename from tests/syntax/fixtures_reference/leading_dots.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/leading_dots.ftl diff --git a/tests/syntax/fixtures_reference/leading_dots.json b/fluent.syntax/tests/syntax/fixtures_reference/leading_dots.json similarity index 100% rename from tests/syntax/fixtures_reference/leading_dots.json rename to fluent.syntax/tests/syntax/fixtures_reference/leading_dots.json diff --git a/tests/syntax/fixtures_reference/literal_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/literal_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.ftl diff --git a/tests/syntax/fixtures_reference/literal_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/literal_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.json diff --git a/tests/syntax/fixtures_reference/member_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/member_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/member_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/member_expressions.ftl diff --git a/tests/syntax/fixtures_reference/member_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/member_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/member_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/member_expressions.json diff --git a/tests/syntax/fixtures_reference/messages.ftl b/fluent.syntax/tests/syntax/fixtures_reference/messages.ftl similarity index 100% rename from tests/syntax/fixtures_reference/messages.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/messages.ftl diff --git a/tests/syntax/fixtures_reference/messages.json b/fluent.syntax/tests/syntax/fixtures_reference/messages.json similarity index 100% rename from tests/syntax/fixtures_reference/messages.json rename to fluent.syntax/tests/syntax/fixtures_reference/messages.json diff --git a/tests/syntax/fixtures_reference/mixed_entries.ftl b/fluent.syntax/tests/syntax/fixtures_reference/mixed_entries.ftl similarity index 100% rename from tests/syntax/fixtures_reference/mixed_entries.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/mixed_entries.ftl diff --git a/tests/syntax/fixtures_reference/mixed_entries.json b/fluent.syntax/tests/syntax/fixtures_reference/mixed_entries.json similarity index 100% rename from tests/syntax/fixtures_reference/mixed_entries.json rename to fluent.syntax/tests/syntax/fixtures_reference/mixed_entries.json diff --git a/tests/syntax/fixtures_reference/multiline_values.ftl b/fluent.syntax/tests/syntax/fixtures_reference/multiline_values.ftl similarity index 100% rename from tests/syntax/fixtures_reference/multiline_values.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/multiline_values.ftl diff --git a/tests/syntax/fixtures_reference/multiline_values.json b/fluent.syntax/tests/syntax/fixtures_reference/multiline_values.json similarity index 100% rename from tests/syntax/fixtures_reference/multiline_values.json rename to fluent.syntax/tests/syntax/fixtures_reference/multiline_values.json diff --git a/tests/syntax/fixtures_reference/placeables.ftl b/fluent.syntax/tests/syntax/fixtures_reference/placeables.ftl similarity index 100% rename from tests/syntax/fixtures_reference/placeables.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/placeables.ftl diff --git a/tests/syntax/fixtures_reference/placeables.json b/fluent.syntax/tests/syntax/fixtures_reference/placeables.json similarity index 100% rename from tests/syntax/fixtures_reference/placeables.json rename to fluent.syntax/tests/syntax/fixtures_reference/placeables.json diff --git a/tests/syntax/fixtures_reference/reference_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/reference_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/reference_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/reference_expressions.ftl diff --git a/tests/syntax/fixtures_reference/reference_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/reference_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/reference_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/reference_expressions.json diff --git a/tests/syntax/fixtures_reference/select_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/select_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_reference/select_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/select_expressions.ftl diff --git a/tests/syntax/fixtures_reference/select_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/select_expressions.json similarity index 100% rename from tests/syntax/fixtures_reference/select_expressions.json rename to fluent.syntax/tests/syntax/fixtures_reference/select_expressions.json diff --git a/tests/syntax/fixtures_reference/select_indent.ftl b/fluent.syntax/tests/syntax/fixtures_reference/select_indent.ftl similarity index 100% rename from tests/syntax/fixtures_reference/select_indent.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/select_indent.ftl diff --git a/tests/syntax/fixtures_reference/select_indent.json b/fluent.syntax/tests/syntax/fixtures_reference/select_indent.json similarity index 100% rename from tests/syntax/fixtures_reference/select_indent.json rename to fluent.syntax/tests/syntax/fixtures_reference/select_indent.json diff --git a/tests/syntax/fixtures_reference/sparse_entries.ftl b/fluent.syntax/tests/syntax/fixtures_reference/sparse_entries.ftl similarity index 100% rename from tests/syntax/fixtures_reference/sparse_entries.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/sparse_entries.ftl diff --git a/tests/syntax/fixtures_reference/sparse_entries.json b/fluent.syntax/tests/syntax/fixtures_reference/sparse_entries.json similarity index 100% rename from tests/syntax/fixtures_reference/sparse_entries.json rename to fluent.syntax/tests/syntax/fixtures_reference/sparse_entries.json diff --git a/tests/syntax/fixtures_reference/tab.ftl b/fluent.syntax/tests/syntax/fixtures_reference/tab.ftl similarity index 100% rename from tests/syntax/fixtures_reference/tab.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/tab.ftl diff --git a/tests/syntax/fixtures_reference/tab.json b/fluent.syntax/tests/syntax/fixtures_reference/tab.json similarity index 100% rename from tests/syntax/fixtures_reference/tab.json rename to fluent.syntax/tests/syntax/fixtures_reference/tab.json diff --git a/tests/syntax/fixtures_reference/term_parameters.ftl b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl similarity index 100% rename from tests/syntax/fixtures_reference/term_parameters.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl diff --git a/tests/syntax/fixtures_reference/term_parameters.json b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json similarity index 100% rename from tests/syntax/fixtures_reference/term_parameters.json rename to fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json diff --git a/tests/syntax/fixtures_reference/terms.ftl b/fluent.syntax/tests/syntax/fixtures_reference/terms.ftl similarity index 100% rename from tests/syntax/fixtures_reference/terms.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/terms.ftl diff --git a/tests/syntax/fixtures_reference/terms.json b/fluent.syntax/tests/syntax/fixtures_reference/terms.json similarity index 100% rename from tests/syntax/fixtures_reference/terms.json rename to fluent.syntax/tests/syntax/fixtures_reference/terms.json diff --git a/tests/syntax/fixtures_reference/variables.ftl b/fluent.syntax/tests/syntax/fixtures_reference/variables.ftl similarity index 100% rename from tests/syntax/fixtures_reference/variables.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/variables.ftl diff --git a/tests/syntax/fixtures_reference/variables.json b/fluent.syntax/tests/syntax/fixtures_reference/variables.json similarity index 100% rename from tests/syntax/fixtures_reference/variables.json rename to fluent.syntax/tests/syntax/fixtures_reference/variables.json diff --git a/tests/syntax/fixtures_reference/variant_keys.ftl b/fluent.syntax/tests/syntax/fixtures_reference/variant_keys.ftl similarity index 100% rename from tests/syntax/fixtures_reference/variant_keys.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/variant_keys.ftl diff --git a/tests/syntax/fixtures_reference/variant_keys.json b/fluent.syntax/tests/syntax/fixtures_reference/variant_keys.json similarity index 100% rename from tests/syntax/fixtures_reference/variant_keys.json rename to fluent.syntax/tests/syntax/fixtures_reference/variant_keys.json diff --git a/tests/syntax/fixtures_reference/variant_lists.ftl b/fluent.syntax/tests/syntax/fixtures_reference/variant_lists.ftl similarity index 100% rename from tests/syntax/fixtures_reference/variant_lists.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/variant_lists.ftl diff --git a/tests/syntax/fixtures_reference/variant_lists.json b/fluent.syntax/tests/syntax/fixtures_reference/variant_lists.json similarity index 100% rename from tests/syntax/fixtures_reference/variant_lists.json rename to fluent.syntax/tests/syntax/fixtures_reference/variant_lists.json diff --git a/tests/syntax/fixtures_reference/variants_indent.ftl b/fluent.syntax/tests/syntax/fixtures_reference/variants_indent.ftl similarity index 100% rename from tests/syntax/fixtures_reference/variants_indent.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/variants_indent.ftl diff --git a/tests/syntax/fixtures_reference/variants_indent.json b/fluent.syntax/tests/syntax/fixtures_reference/variants_indent.json similarity index 100% rename from tests/syntax/fixtures_reference/variants_indent.json rename to fluent.syntax/tests/syntax/fixtures_reference/variants_indent.json diff --git a/tests/syntax/fixtures_reference/whitespace_in_value.ftl b/fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.ftl similarity index 100% rename from tests/syntax/fixtures_reference/whitespace_in_value.ftl rename to fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.ftl diff --git a/tests/syntax/fixtures_reference/whitespace_in_value.json b/fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.json similarity index 100% rename from tests/syntax/fixtures_reference/whitespace_in_value.json rename to fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.json diff --git a/tests/syntax/fixtures_structure/attribute_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/attribute_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_structure/attribute_with_empty_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/attribute_with_empty_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.json diff --git a/tests/syntax/fixtures_structure/blank_lines.ftl b/fluent.syntax/tests/syntax/fixtures_structure/blank_lines.ftl similarity index 100% rename from tests/syntax/fixtures_structure/blank_lines.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/blank_lines.ftl diff --git a/tests/syntax/fixtures_structure/blank_lines.json b/fluent.syntax/tests/syntax/fixtures_structure/blank_lines.json similarity index 100% rename from tests/syntax/fixtures_structure/blank_lines.json rename to fluent.syntax/tests/syntax/fixtures_structure/blank_lines.json diff --git a/tests/syntax/fixtures_structure/crlf.ftl b/fluent.syntax/tests/syntax/fixtures_structure/crlf.ftl similarity index 100% rename from tests/syntax/fixtures_structure/crlf.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/crlf.ftl diff --git a/tests/syntax/fixtures_structure/crlf.json b/fluent.syntax/tests/syntax/fixtures_structure/crlf.json similarity index 100% rename from tests/syntax/fixtures_structure/crlf.json rename to fluent.syntax/tests/syntax/fixtures_structure/crlf.json diff --git a/tests/syntax/fixtures_structure/dash_at_eof.ftl b/fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.ftl similarity index 100% rename from tests/syntax/fixtures_structure/dash_at_eof.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.ftl diff --git a/tests/syntax/fixtures_structure/dash_at_eof.json b/fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.json similarity index 100% rename from tests/syntax/fixtures_structure/dash_at_eof.json rename to fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.json diff --git a/tests/syntax/fixtures_structure/elements_indent.ftl b/fluent.syntax/tests/syntax/fixtures_structure/elements_indent.ftl similarity index 100% rename from tests/syntax/fixtures_structure/elements_indent.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/elements_indent.ftl diff --git a/tests/syntax/fixtures_structure/elements_indent.json b/fluent.syntax/tests/syntax/fixtures_structure/elements_indent.json similarity index 100% rename from tests/syntax/fixtures_structure/elements_indent.json rename to fluent.syntax/tests/syntax/fixtures_structure/elements_indent.json diff --git a/tests/syntax/fixtures_structure/escape_sequences.ftl b/fluent.syntax/tests/syntax/fixtures_structure/escape_sequences.ftl similarity index 100% rename from tests/syntax/fixtures_structure/escape_sequences.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/escape_sequences.ftl diff --git a/tests/syntax/fixtures_structure/escape_sequences.json b/fluent.syntax/tests/syntax/fixtures_structure/escape_sequences.json similarity index 100% rename from tests/syntax/fixtures_structure/escape_sequences.json rename to fluent.syntax/tests/syntax/fixtures_structure/escape_sequences.json diff --git a/tests/syntax/fixtures_structure/expressions_call_args.ftl b/fluent.syntax/tests/syntax/fixtures_structure/expressions_call_args.ftl similarity index 100% rename from tests/syntax/fixtures_structure/expressions_call_args.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/expressions_call_args.ftl diff --git a/tests/syntax/fixtures_structure/expressions_call_args.json b/fluent.syntax/tests/syntax/fixtures_structure/expressions_call_args.json similarity index 100% rename from tests/syntax/fixtures_structure/expressions_call_args.json rename to fluent.syntax/tests/syntax/fixtures_structure/expressions_call_args.json diff --git a/tests/syntax/fixtures_structure/junk.ftl b/fluent.syntax/tests/syntax/fixtures_structure/junk.ftl similarity index 100% rename from tests/syntax/fixtures_structure/junk.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/junk.ftl diff --git a/tests/syntax/fixtures_structure/junk.json b/fluent.syntax/tests/syntax/fixtures_structure/junk.json similarity index 100% rename from tests/syntax/fixtures_structure/junk.json rename to fluent.syntax/tests/syntax/fixtures_structure/junk.json diff --git a/tests/syntax/fixtures_structure/leading_dots.ftl b/fluent.syntax/tests/syntax/fixtures_structure/leading_dots.ftl similarity index 100% rename from tests/syntax/fixtures_structure/leading_dots.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/leading_dots.ftl diff --git a/tests/syntax/fixtures_structure/leading_dots.json b/fluent.syntax/tests/syntax/fixtures_structure/leading_dots.json similarity index 100% rename from tests/syntax/fixtures_structure/leading_dots.json rename to fluent.syntax/tests/syntax/fixtures_structure/leading_dots.json diff --git a/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.ftl diff --git a/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.json diff --git a/tests/syntax/fixtures_structure/message_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/message_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_structure/message_with_empty_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/message_with_empty_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_pattern.json diff --git a/tests/syntax/fixtures_structure/multiline-comment.ftl b/fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.ftl similarity index 100% rename from tests/syntax/fixtures_structure/multiline-comment.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.ftl diff --git a/tests/syntax/fixtures_structure/multiline-comment.json b/fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.json similarity index 100% rename from tests/syntax/fixtures_structure/multiline-comment.json rename to fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.json diff --git a/tests/syntax/fixtures_structure/multiline_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/multiline_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/multiline_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/multiline_pattern.ftl diff --git a/tests/syntax/fixtures_structure/multiline_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/multiline_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/multiline_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/multiline_pattern.json diff --git a/tests/syntax/fixtures_structure/placeable_at_eol.ftl b/fluent.syntax/tests/syntax/fixtures_structure/placeable_at_eol.ftl similarity index 100% rename from tests/syntax/fixtures_structure/placeable_at_eol.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/placeable_at_eol.ftl diff --git a/tests/syntax/fixtures_structure/placeable_at_eol.json b/fluent.syntax/tests/syntax/fixtures_structure/placeable_at_eol.json similarity index 100% rename from tests/syntax/fixtures_structure/placeable_at_eol.json rename to fluent.syntax/tests/syntax/fixtures_structure/placeable_at_eol.json diff --git a/tests/syntax/fixtures_structure/resource_comment.ftl b/fluent.syntax/tests/syntax/fixtures_structure/resource_comment.ftl similarity index 100% rename from tests/syntax/fixtures_structure/resource_comment.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/resource_comment.ftl diff --git a/tests/syntax/fixtures_structure/resource_comment.json b/fluent.syntax/tests/syntax/fixtures_structure/resource_comment.json similarity index 100% rename from tests/syntax/fixtures_structure/resource_comment.json rename to fluent.syntax/tests/syntax/fixtures_structure/resource_comment.json diff --git a/tests/syntax/fixtures_structure/resource_comment_trailing_line.ftl b/fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.ftl similarity index 100% rename from tests/syntax/fixtures_structure/resource_comment_trailing_line.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.ftl diff --git a/tests/syntax/fixtures_structure/resource_comment_trailing_line.json b/fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.json similarity index 100% rename from tests/syntax/fixtures_structure/resource_comment_trailing_line.json rename to fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.json diff --git a/tests/syntax/fixtures_structure/section.ftl b/fluent.syntax/tests/syntax/fixtures_structure/section.ftl similarity index 100% rename from tests/syntax/fixtures_structure/section.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/section.ftl diff --git a/tests/syntax/fixtures_structure/section.json b/fluent.syntax/tests/syntax/fixtures_structure/section.json similarity index 100% rename from tests/syntax/fixtures_structure/section.json rename to fluent.syntax/tests/syntax/fixtures_structure/section.json diff --git a/tests/syntax/fixtures_structure/section_with_comment.ftl b/fluent.syntax/tests/syntax/fixtures_structure/section_with_comment.ftl similarity index 100% rename from tests/syntax/fixtures_structure/section_with_comment.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/section_with_comment.ftl diff --git a/tests/syntax/fixtures_structure/section_with_comment.json b/fluent.syntax/tests/syntax/fixtures_structure/section_with_comment.json similarity index 100% rename from tests/syntax/fixtures_structure/section_with_comment.json rename to fluent.syntax/tests/syntax/fixtures_structure/section_with_comment.json diff --git a/tests/syntax/fixtures_structure/select_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_structure/select_expressions.ftl similarity index 100% rename from tests/syntax/fixtures_structure/select_expressions.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/select_expressions.ftl diff --git a/tests/syntax/fixtures_structure/select_expressions.json b/fluent.syntax/tests/syntax/fixtures_structure/select_expressions.json similarity index 100% rename from tests/syntax/fixtures_structure/select_expressions.json rename to fluent.syntax/tests/syntax/fixtures_structure/select_expressions.json diff --git a/tests/syntax/fixtures_structure/simple_message.ftl b/fluent.syntax/tests/syntax/fixtures_structure/simple_message.ftl similarity index 100% rename from tests/syntax/fixtures_structure/simple_message.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/simple_message.ftl diff --git a/tests/syntax/fixtures_structure/simple_message.json b/fluent.syntax/tests/syntax/fixtures_structure/simple_message.json similarity index 100% rename from tests/syntax/fixtures_structure/simple_message.json rename to fluent.syntax/tests/syntax/fixtures_structure/simple_message.json diff --git a/tests/syntax/fixtures_structure/sparse-messages.ftl b/fluent.syntax/tests/syntax/fixtures_structure/sparse-messages.ftl similarity index 100% rename from tests/syntax/fixtures_structure/sparse-messages.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/sparse-messages.ftl diff --git a/tests/syntax/fixtures_structure/sparse-messages.json b/fluent.syntax/tests/syntax/fixtures_structure/sparse-messages.json similarity index 100% rename from tests/syntax/fixtures_structure/sparse-messages.json rename to fluent.syntax/tests/syntax/fixtures_structure/sparse-messages.json diff --git a/tests/syntax/fixtures_structure/standalone_comment.ftl b/fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.ftl similarity index 100% rename from tests/syntax/fixtures_structure/standalone_comment.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.ftl diff --git a/tests/syntax/fixtures_structure/standalone_comment.json b/fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.json similarity index 100% rename from tests/syntax/fixtures_structure/standalone_comment.json rename to fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.json diff --git a/tests/syntax/fixtures_structure/syntax_zero_four.ftl b/fluent.syntax/tests/syntax/fixtures_structure/syntax_zero_four.ftl similarity index 100% rename from tests/syntax/fixtures_structure/syntax_zero_four.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/syntax_zero_four.ftl diff --git a/tests/syntax/fixtures_structure/syntax_zero_four.json b/fluent.syntax/tests/syntax/fixtures_structure/syntax_zero_four.json similarity index 100% rename from tests/syntax/fixtures_structure/syntax_zero_four.json rename to fluent.syntax/tests/syntax/fixtures_structure/syntax_zero_four.json diff --git a/tests/syntax/fixtures_structure/term.ftl b/fluent.syntax/tests/syntax/fixtures_structure/term.ftl similarity index 100% rename from tests/syntax/fixtures_structure/term.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/term.ftl diff --git a/tests/syntax/fixtures_structure/term.json b/fluent.syntax/tests/syntax/fixtures_structure/term.json similarity index 100% rename from tests/syntax/fixtures_structure/term.json rename to fluent.syntax/tests/syntax/fixtures_structure/term.json diff --git a/tests/syntax/fixtures_structure/term_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/term_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_structure/term_with_empty_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/term_with_empty_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.json diff --git a/tests/syntax/fixtures_structure/unclosed.ftl b/fluent.syntax/tests/syntax/fixtures_structure/unclosed.ftl similarity index 100% rename from tests/syntax/fixtures_structure/unclosed.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/unclosed.ftl diff --git a/tests/syntax/fixtures_structure/unclosed.json b/fluent.syntax/tests/syntax/fixtures_structure/unclosed.json similarity index 100% rename from tests/syntax/fixtures_structure/unclosed.json rename to fluent.syntax/tests/syntax/fixtures_structure/unclosed.json diff --git a/tests/syntax/fixtures_structure/variant_keys.ftl b/fluent.syntax/tests/syntax/fixtures_structure/variant_keys.ftl similarity index 100% rename from tests/syntax/fixtures_structure/variant_keys.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/variant_keys.ftl diff --git a/tests/syntax/fixtures_structure/variant_keys.json b/fluent.syntax/tests/syntax/fixtures_structure/variant_keys.json similarity index 100% rename from tests/syntax/fixtures_structure/variant_keys.json rename to fluent.syntax/tests/syntax/fixtures_structure/variant_keys.json diff --git a/tests/syntax/fixtures_structure/variant_with_empty_pattern.ftl b/fluent.syntax/tests/syntax/fixtures_structure/variant_with_empty_pattern.ftl similarity index 100% rename from tests/syntax/fixtures_structure/variant_with_empty_pattern.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/variant_with_empty_pattern.ftl diff --git a/tests/syntax/fixtures_structure/variant_with_empty_pattern.json b/fluent.syntax/tests/syntax/fixtures_structure/variant_with_empty_pattern.json similarity index 100% rename from tests/syntax/fixtures_structure/variant_with_empty_pattern.json rename to fluent.syntax/tests/syntax/fixtures_structure/variant_with_empty_pattern.json diff --git a/tests/syntax/fixtures_structure/whitespace_leading.ftl b/fluent.syntax/tests/syntax/fixtures_structure/whitespace_leading.ftl similarity index 100% rename from tests/syntax/fixtures_structure/whitespace_leading.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/whitespace_leading.ftl diff --git a/tests/syntax/fixtures_structure/whitespace_leading.json b/fluent.syntax/tests/syntax/fixtures_structure/whitespace_leading.json similarity index 100% rename from tests/syntax/fixtures_structure/whitespace_leading.json rename to fluent.syntax/tests/syntax/fixtures_structure/whitespace_leading.json diff --git a/tests/syntax/fixtures_structure/whitespace_trailing.ftl b/fluent.syntax/tests/syntax/fixtures_structure/whitespace_trailing.ftl similarity index 100% rename from tests/syntax/fixtures_structure/whitespace_trailing.ftl rename to fluent.syntax/tests/syntax/fixtures_structure/whitespace_trailing.ftl diff --git a/tests/syntax/fixtures_structure/whitespace_trailing.json b/fluent.syntax/tests/syntax/fixtures_structure/whitespace_trailing.json similarity index 100% rename from tests/syntax/fixtures_structure/whitespace_trailing.json rename to fluent.syntax/tests/syntax/fixtures_structure/whitespace_trailing.json diff --git a/tests/syntax/test_ast_json.py b/fluent.syntax/tests/syntax/test_ast_json.py similarity index 100% rename from tests/syntax/test_ast_json.py rename to fluent.syntax/tests/syntax/test_ast_json.py diff --git a/tests/syntax/test_behavior.py b/fluent.syntax/tests/syntax/test_behavior.py similarity index 100% rename from tests/syntax/test_behavior.py rename to fluent.syntax/tests/syntax/test_behavior.py diff --git a/tests/syntax/test_entry.py b/fluent.syntax/tests/syntax/test_entry.py similarity index 100% rename from tests/syntax/test_entry.py rename to fluent.syntax/tests/syntax/test_entry.py diff --git a/tests/syntax/test_equals.py b/fluent.syntax/tests/syntax/test_equals.py similarity index 100% rename from tests/syntax/test_equals.py rename to fluent.syntax/tests/syntax/test_equals.py diff --git a/tests/syntax/test_reference.py b/fluent.syntax/tests/syntax/test_reference.py similarity index 100% rename from tests/syntax/test_reference.py rename to fluent.syntax/tests/syntax/test_reference.py diff --git a/tests/syntax/test_serializer.py b/fluent.syntax/tests/syntax/test_serializer.py similarity index 98% rename from tests/syntax/test_serializer.py rename to fluent.syntax/tests/syntax/test_serializer.py index e9d3831d..57c8cd6a 100644 --- a/tests/syntax/test_serializer.py +++ b/fluent.syntax/tests/syntax/test_serializer.py @@ -76,7 +76,7 @@ def test_term_reference(self): """ self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) - def test_external_argument(self): + def test_variable_reference(self): input = """\ foo = Foo { $bar } """ @@ -327,7 +327,7 @@ def test_select_expression_nested(self): """ self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) - def test_selector_external_argument(self): + def test_selector_variable_reference(self): input = """\ foo = { $bar -> @@ -387,7 +387,7 @@ def test_call_expression_with_message_reference(self): """ self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) - def test_call_expression_with_external_argument(self): + def test_call_expression_with_variable_reference(self): input = """\ foo = { FOO($bar) } """ @@ -490,7 +490,7 @@ def test_message_reference(self): """ self.assertEqual(self.pretty_expr(input), 'msg') - def test_external_argument(self): + def test_variable_reference(self): input = """\ foo = { $ext } """ diff --git a/tests/syntax/test_stream.py b/fluent.syntax/tests/syntax/test_stream.py similarity index 100% rename from tests/syntax/test_stream.py rename to fluent.syntax/tests/syntax/test_stream.py diff --git a/tests/syntax/test_structure.py b/fluent.syntax/tests/syntax/test_structure.py similarity index 100% rename from tests/syntax/test_structure.py rename to fluent.syntax/tests/syntax/test_structure.py diff --git a/fluent.syntax/tox.ini b/fluent.syntax/tox.ini new file mode 100644 index 00000000..08e5a27f --- /dev/null +++ b/fluent.syntax/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py27, py35, py36, pypy, pypy3 +skipsdist=True + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + six +commands = ./runtests.py diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e50b967a..00000000 --- a/tox.ini +++ /dev/null @@ -1,9 +0,0 @@ -[tox] -envlist = py27, py35, py36 -skipsdist=True - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = six -commands = python -m unittest discover -s tests/syntax