├── .flake8 ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.rst ├── dev-requirements.txt ├── docs ├── _static │ ├── project-fluent.css │ └── versions.js ├── _templates │ ├── layout.html │ └── versions.html ├── conf.py ├── index.rst ├── requirements.in └── requirements.txt ├── fluent.docs ├── README.rst ├── fluent │ └── docs │ │ ├── __init__.py │ │ ├── build.py │ │ └── tags.py └── setup.py ├── fluent.pygments ├── CHANGELOG.rst ├── README.rst ├── docs │ └── index.rst ├── fluent │ └── pygments │ │ ├── __init__.py │ │ ├── cli.py │ │ └── lexer.py ├── setup.cfg ├── setup.py └── tests │ ├── __init__.py │ └── pygments │ ├── __init__.py │ └── test_lexer.py ├── fluent.runtime ├── CHANGELOG.rst ├── README.rst ├── docs │ ├── history.rst │ ├── index.rst │ ├── installation.rst │ ├── internals.rst │ ├── make.bat │ ├── reference.rst │ ├── requirements.txt │ └── usage.rst ├── fluent │ └── runtime │ │ ├── __init__.py │ │ ├── builtins.py │ │ ├── bundle.py │ │ ├── errors.py │ │ ├── fallback.py │ │ ├── prepare.py │ │ ├── py.typed │ │ ├── resolver.py │ │ ├── types.py │ │ └── utils.py ├── setup.cfg ├── setup.py ├── tests │ ├── __init__.py │ ├── format │ │ ├── __init__.py │ │ ├── test_arguments.py │ │ ├── test_attributes.py │ │ ├── test_builtins.py │ │ ├── test_functions.py │ │ ├── test_isolating.py │ │ ├── test_parameterized_terms.py │ │ ├── test_placeables.py │ │ ├── test_primitives.py │ │ └── test_select_expression.py │ ├── test_bomb.py │ ├── test_bundle.py │ ├── test_fallback.py │ ├── test_types.py │ ├── test_utils.py │ └── utils.py ├── tools │ └── benchmarks │ │ ├── README.md │ │ ├── fluent_benchmark.py │ │ └── requirements.txt └── tox.ini ├── fluent.syntax ├── .gitattributes ├── CHANGELOG.md ├── README.rst ├── docs │ ├── ast.rst │ ├── index.rst │ ├── parsing.rst │ ├── reference.rst │ ├── serializing.rst │ ├── usage.rst │ └── visitor.rst ├── fluent │ └── syntax │ │ ├── __init__.py │ │ ├── ast.py │ │ ├── errors.py │ │ ├── parser.py │ │ ├── py.typed │ │ ├── serializer.py │ │ ├── stream.py │ │ └── visitor.py ├── setup.cfg ├── setup.py ├── tests │ ├── __init__.py │ └── syntax │ │ ├── README.md │ │ ├── __init__.py │ │ ├── fixtures_perf │ │ └── workload-low.ftl │ │ ├── fixtures_reference │ │ ├── any_char.ftl │ │ ├── any_char.json │ │ ├── astral.ftl │ │ ├── astral.json │ │ ├── call_expressions.ftl │ │ ├── call_expressions.json │ │ ├── callee_expressions.ftl │ │ ├── callee_expressions.json │ │ ├── comments.ftl │ │ ├── comments.json │ │ ├── cr.ftl │ │ ├── cr.json │ │ ├── crlf.ftl │ │ ├── crlf.json │ │ ├── eof_comment.ftl │ │ ├── eof_comment.json │ │ ├── eof_empty.ftl │ │ ├── eof_empty.json │ │ ├── eof_id.ftl │ │ ├── eof_id.json │ │ ├── eof_id_equals.ftl │ │ ├── eof_id_equals.json │ │ ├── eof_junk.ftl │ │ ├── eof_junk.json │ │ ├── eof_value.ftl │ │ ├── eof_value.json │ │ ├── escaped_characters.ftl │ │ ├── escaped_characters.json │ │ ├── junk.ftl │ │ ├── junk.json │ │ ├── leading_dots.ftl │ │ ├── leading_dots.json │ │ ├── literal_expressions.ftl │ │ ├── literal_expressions.json │ │ ├── member_expressions.ftl │ │ ├── member_expressions.json │ │ ├── messages.ftl │ │ ├── messages.json │ │ ├── mixed_entries.ftl │ │ ├── mixed_entries.json │ │ ├── multiline_values.ftl │ │ ├── multiline_values.json │ │ ├── numbers.ftl │ │ ├── numbers.json │ │ ├── obsolete.ftl │ │ ├── obsolete.json │ │ ├── placeables.ftl │ │ ├── placeables.json │ │ ├── reference_expressions.ftl │ │ ├── reference_expressions.json │ │ ├── select_expressions.ftl │ │ ├── select_expressions.json │ │ ├── select_indent.ftl │ │ ├── select_indent.json │ │ ├── sparse_entries.ftl │ │ ├── sparse_entries.json │ │ ├── special_chars.ftl │ │ ├── special_chars.json │ │ ├── tab.ftl │ │ ├── tab.json │ │ ├── term_parameters.ftl │ │ ├── term_parameters.json │ │ ├── terms.ftl │ │ ├── terms.json │ │ ├── variables.ftl │ │ ├── variables.json │ │ ├── variant_keys.ftl │ │ ├── variant_keys.json │ │ ├── whitespace_in_value.ftl │ │ ├── whitespace_in_value.json │ │ ├── zero_length.ftl │ │ └── zero_length.json │ │ ├── fixtures_structure │ │ ├── attribute_expression_with_wrong_attr.ftl │ │ ├── attribute_expression_with_wrong_attr.json │ │ ├── attribute_of_private_as_placeable.ftl │ │ ├── attribute_of_private_as_placeable.json │ │ ├── attribute_of_public_as_selector.ftl │ │ ├── attribute_of_public_as_selector.json │ │ ├── attribute_starts_from_nl.ftl │ │ ├── attribute_starts_from_nl.json │ │ ├── attribute_with_empty_pattern.ftl │ │ ├── attribute_with_empty_pattern.json │ │ ├── attribute_without_equal_sign.ftl │ │ ├── attribute_without_equal_sign.json │ │ ├── blank_lines.ftl │ │ ├── blank_lines.json │ │ ├── broken_number.ftl │ │ ├── broken_number.json │ │ ├── call_expression_errors.ftl │ │ ├── call_expression_errors.json │ │ ├── comment_with_eof.ftl │ │ ├── comment_with_eof.json │ │ ├── crlf.ftl │ │ ├── crlf.json │ │ ├── dash_at_eof.ftl │ │ ├── dash_at_eof.json │ │ ├── elements_indent.ftl │ │ ├── elements_indent.json │ │ ├── empty_resource.ftl │ │ ├── empty_resource.json │ │ ├── empty_resource_with_ws.ftl │ │ ├── empty_resource_with_ws.json │ │ ├── escape_sequences.ftl │ │ ├── escape_sequences.json │ │ ├── expressions_call_args.ftl │ │ ├── expressions_call_args.json │ │ ├── indent.ftl │ │ ├── indent.json │ │ ├── junk.ftl │ │ ├── junk.json │ │ ├── leading_dots.ftl │ │ ├── leading_dots.json │ │ ├── leading_empty_lines.ftl │ │ ├── leading_empty_lines.json │ │ ├── leading_empty_lines_with_ws.ftl │ │ ├── leading_empty_lines_with_ws.json │ │ ├── message_reference_as_selector.ftl │ │ ├── message_reference_as_selector.json │ │ ├── message_with_empty_multiline_pattern.ftl │ │ ├── message_with_empty_multiline_pattern.json │ │ ├── message_with_empty_pattern.ftl │ │ ├── message_with_empty_pattern.json │ │ ├── multiline-comment.ftl │ │ ├── multiline-comment.json │ │ ├── multiline_pattern.ftl │ │ ├── multiline_pattern.json │ │ ├── multiline_string.ftl │ │ ├── multiline_string.json │ │ ├── multiline_with_non_empty_first_line.ftl │ │ ├── multiline_with_non_empty_first_line.json │ │ ├── multiline_with_placeables.ftl │ │ ├── multiline_with_placeables.json │ │ ├── non_id_attribute_name.ftl │ │ ├── non_id_attribute_name.json │ │ ├── placeable_at_eol.ftl │ │ ├── placeable_at_eol.json │ │ ├── placeable_at_line_extremes.ftl │ │ ├── placeable_at_line_extremes.json │ │ ├── placeable_in_placeable.ftl │ │ ├── placeable_in_placeable.json │ │ ├── placeable_without_close_bracket.ftl │ │ ├── placeable_without_close_bracket.json │ │ ├── resource_comment.ftl │ │ ├── resource_comment.json │ │ ├── resource_comment_trailing_line.ftl │ │ ├── resource_comment_trailing_line.json │ │ ├── second_attribute_starts_from_nl.ftl │ │ ├── second_attribute_starts_from_nl.json │ │ ├── select_expression_with_two_selectors.ftl │ │ ├── select_expression_with_two_selectors.json │ │ ├── select_expression_without_arrow.ftl │ │ ├── select_expression_without_arrow.json │ │ ├── select_expression_without_variants.ftl │ │ ├── select_expression_without_variants.json │ │ ├── select_expressions.ftl │ │ ├── select_expressions.json │ │ ├── simple_message.ftl │ │ ├── simple_message.json │ │ ├── single_char_id.ftl │ │ ├── single_char_id.json │ │ ├── sparse-messages.ftl │ │ ├── sparse-messages.json │ │ ├── standalone_comment.ftl │ │ ├── standalone_comment.json │ │ ├── standalone_identifier.ftl │ │ ├── standalone_identifier.json │ │ ├── term.ftl │ │ ├── term.json │ │ ├── term_with_empty_pattern.ftl │ │ ├── term_with_empty_pattern.json │ │ ├── unclosed.ftl │ │ ├── unclosed.json │ │ ├── unclosed_empty_placeable_error.ftl │ │ ├── unclosed_empty_placeable_error.json │ │ ├── unknown_entry_start.ftl │ │ ├── unknown_entry_start.json │ │ ├── variant_ends_abruptly.ftl │ │ ├── variant_ends_abruptly.json │ │ ├── variant_keys.ftl │ │ ├── variant_keys.json │ │ ├── variant_starts_from_nl.ftl │ │ ├── variant_starts_from_nl.json │ │ ├── variant_with_digit_key.ftl │ │ ├── variant_with_digit_key.json │ │ ├── variant_with_empty_pattern.ftl │ │ ├── variant_with_empty_pattern.json │ │ ├── variant_with_leading_space_in_name.ftl │ │ ├── variant_with_leading_space_in_name.json │ │ ├── variant_with_symbol_with_space.ftl │ │ ├── variant_with_symbol_with_space.json │ │ ├── variants_with_two_defaults.ftl │ │ ├── variants_with_two_defaults.json │ │ ├── whitespace_leading.ftl │ │ ├── whitespace_leading.json │ │ ├── whitespace_trailing.ftl │ │ └── whitespace_trailing.json │ │ ├── test_ast_json.py │ │ ├── test_entry.py │ │ ├── test_equals.py │ │ ├── test_literal.py │ │ ├── test_reference.py │ │ ├── test_serializer.py │ │ ├── test_stream.py │ │ ├── test_structure.py │ │ └── test_visitor.py └── tox.ini ├── pyproject.toml ├── scripts └── build-docs └── tools ├── fluentfmt.py ├── parse.py └── serialize.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=.tox 3 | extend-ignore = E203 4 | max-line-length=120 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ftl eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | cache: pip 17 | cache-dependency-path: | 18 | dev-requirements.txt 19 | fluent.syntax/setup.py 20 | fluent.runtime/setup.py 21 | - run: python -m pip install -r dev-requirements.txt 22 | - run: python -m pip install ./fluent.syntax ./fluent.runtime 23 | - run: python -m flake8 24 | - run: python -m mypy fluent.syntax/fluent fluent.runtime/fluent 25 | test: 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [ubuntu-22.04, windows-2022] 30 | python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | cache: pip 37 | cache-dependency-path: | 38 | fluent.syntax/setup.py 39 | fluent.runtime/setup.py 40 | - run: python -m pip install ./fluent.syntax ./fluent.runtime 41 | - run: python -m unittest discover -s fluent.syntax 42 | - run: python -m unittest discover -s fluent.runtime 43 | 44 | # Test compatibility with the oldest Python version we claim to support, 45 | # and for fluent.runtime's compatibility with a range of fluent.syntax versions. 46 | compatibility: 47 | runs-on: ubuntu-20.04 # https://github.com/actions/setup-python/issues/544 48 | strategy: 49 | matrix: 50 | fluent-syntax: 51 | - ./fluent.syntax 52 | - fluent.syntax==0.19.0 53 | - fluent.syntax==0.18.1 six 54 | - fluent.syntax==0.17.0 six 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: actions/setup-python@v4 58 | with: 59 | python-version: 3.6 60 | - run: python -m pip install ${{ matrix.fluent-syntax }} 61 | - run: python -m pip install ./fluent.runtime 62 | - run: python -m unittest discover -s fluent.runtime 63 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | # Trigger the workflow on push or pull request, 6 | # but only for the main branch 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - docs/** 12 | - fluent.*/docs/** 13 | - fluent.*/fluent/** 14 | - fluent.*/setup.cfg 15 | pull_request: 16 | branches: 17 | - main 18 | paths: 19 | - docs/** 20 | - fluent.*/docs/** 21 | - fluent.*/fluent/** 22 | - fluent.*/setup.cfg 23 | 24 | jobs: 25 | build: 26 | name: build 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | - uses: actions/setup-python@v5 33 | with: 34 | python-version: 3.9 35 | - name: Install dependencies 36 | run: | 37 | pip install -r docs/requirements.txt 38 | pip install ./fluent.docs 39 | - run: ./scripts/build-docs python-fluent 40 | - uses: actions/upload-artifact@v4 41 | with: 42 | name: html 43 | path: | 44 | _build/python-fluent 45 | !_build/**/.buildinfo 46 | 47 | publish: 48 | name: publish 49 | needs: [build] 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: actions/download-artifact@v4 54 | with: 55 | name: html 56 | path: _build 57 | - name: Deploy 🚀 58 | uses: JamesIves/github-pages-deploy-action@v4.4.1 59 | with: 60 | branch: gh-pages # The branch the action should deploy to. 61 | folder: _build # The folder the action should deploy. 62 | clean: true # Automatically remove deleted files from the deploy branch 63 | dry-run: ${{ github.event_name == 'pull_request' }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.pyc 3 | *.egg-info/ 4 | _build 5 | build 6 | dist 7 | /fluent.*/docs/_templates/versions.html 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette 4 | guidelines. For more details, please read the [Mozilla Community Participation 5 | Guidelines][]. 6 | 7 | 8 | ## How to Report 9 | 10 | For more information on how to report violations of the Community Participation 11 | Guidelines, please read our [How to Report][] page. 12 | 13 | 14 | [Mozilla Community Participation Guidelines]: https://www.mozilla.org/about/governance/policies/participation/ 15 | [How to Report]: https://www.mozilla.org/about/governance/policies/participation/reporting/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Mozilla Foundation 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Project Fluent 2 | ============== 3 | 4 | This is a collection of Python packages to use the `Fluent localization 5 | system `__. 6 | 7 | python-fluent consists of these packages: 8 | 9 | ``fluent.syntax`` 10 | ----------------- 11 | 12 | The `syntax package `_ includes the parser, serializer, and traversal 13 | utilities like Visitor and Transformer. You’re looking for this package 14 | if you work on tooling for Fluent in Python. 15 | 16 | ``fluent.runtime`` 17 | ------------------ 18 | 19 | The `runtime package `__ includes the library required to use Fluent to localize 20 | your Python application. It comes with a ``Localization`` class to use, 21 | based on an implementation of ``FluentBundle``. It uses the tooling parser above 22 | to read Fluent files. 23 | 24 | ``fluent.pygments`` 25 | ------------------- 26 | 27 | A `plugin for pygments `_ to add syntax highlighting to Sphinx. 28 | 29 | Discuss 30 | ------- 31 | 32 | We’d love to hear your thoughts on Project Fluent! Whether you’re a 33 | localizer looking for a better way to express yourself in your language, 34 | or a developer trying to make your app localizable and multilingual, or 35 | a hacker looking for a project to contribute to, please do get in touch 36 | on the mailing list and the IRC channel. 37 | 38 | - Mozilla Discourse: https://discourse.mozilla.org/c/fluent 39 | - Matrix channel: 40 | `#fluent:mozilla.org `__ 41 | 42 | Get Involved 43 | ------------ 44 | 45 | python-fluent is open-source, licensed under the Apache License, Version 46 | 2.0. We encourage everyone to take a look at our code and we’ll listen 47 | to your feedback. 48 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | black ~= 24.0 2 | flake8 ~= 7.0 3 | isort ~= 5.0 4 | mypy ~= 1.0 5 | tox ~= 4.0 6 | types-babel 7 | types-pytz 8 | -------------------------------------------------------------------------------- /docs/_static/project-fluent.css: -------------------------------------------------------------------------------- 1 | div.related { 2 | background-color: #356eb7; 3 | } 4 | 5 | #versions { 6 | position: relative; 7 | color: #444; 8 | } 9 | 10 | #versions > span.version { 11 | cursor: pointer; 12 | } 13 | 14 | #versions > span.version::after { 15 | content: " ⮞"; 16 | } 17 | 18 | #versions.opened > span.version { 19 | display: none; 20 | } 21 | 22 | #versions:not(.opened) > span.links { 23 | display: none; 24 | } 25 | -------------------------------------------------------------------------------- /docs/_static/versions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Show/hide versions when clicking on the versions paragraph 3 | */ 4 | 5 | $(function() { 6 | let versions = document.getElementById('versions'); 7 | if (versions) { 8 | versions.onclick = function(ev) { 9 | this.classList.toggle('opened'); 10 | } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {%- block css %} 4 | 5 | 6 | {%- for css in css_files %} 7 | 8 | {%- endfor %} 9 | {%- endblock %} 10 | 11 | {%- block scripts %} 12 | 13 | {%- for js in script_files %} 14 | 15 | {%- endfor %} 16 | {%- endblock %} 17 | 18 | {%- block footer %} 19 | 22 | {%- endblock %} 23 | -------------------------------------------------------------------------------- /docs/_templates/versions.html: -------------------------------------------------------------------------------- 1 | {# 2 | Intentionally left blank. 3 | Project documtations fill in a sidebar control as part of the build process. 4 | #} 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "python-fluent" 21 | 22 | 23 | # -- General configuration --------------------------------------------------- 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 27 | # ones. 28 | extensions = [ 29 | "sphinx.ext.intersphinx", 30 | "sphinx.ext.viewcode", 31 | "sphinx.ext.autodoc", 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | 38 | # Add src_dir/docs/_templates in a hook as we only have the src_dir then. 39 | def setup(app): 40 | app.connect("config-inited", add_templates) 41 | 42 | 43 | def add_templates(app, config): 44 | config.templates_path.insert(0, f"{app.srcdir}/_templates") 45 | 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = "nature" 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ["_static"] 64 | html_css_files = ["project-fluent.css"] 65 | html_js_files = ["versions.js"] 66 | 67 | html_sidebars = { 68 | "**": ["globaltoc.html", "versions.html", "searchbox.html"], 69 | } 70 | html_theme_options = {} 71 | 72 | # -- Extension configuration ------------------------------------------------- 73 | 74 | # -- Options for intersphinx extension --------------------------------------- 75 | 76 | # Example configuration for intersphinx: refer to the Python standard library. 77 | intersphinx_mapping = {} 78 | 79 | # -- Options for autodoc extension -------------------------------------------- 80 | autodoc_mock_imports = [ 81 | "attr", 82 | ] 83 | autodoc_member_order = "bysource" 84 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | python-fluent 3 | ============= 4 | 5 | The :py:mod:`python-fluent` project contains several packages to 6 | bring `Project Fluent `__ to Python. 7 | Visit the `Github project `__ 8 | for the full list. Three packages in particular are of general interest 9 | and documented here. 10 | 11 | fluent.syntax 12 | ------------- 13 | 14 | ``fluent.syntax`` is the package to use for tooling, analysis, and 15 | processing of Fluent files. 16 | 17 | fluent.runtime 18 | -------------- 19 | 20 | ``fluent.runtime`` is the reference runtime implementation for 21 | Fluent in Python. 22 | 23 | fluent.pygments 24 | --------------- 25 | 26 | ``fluent.pygments`` can be used to add syntax highlighting for 27 | Fluent files to Sphinx documentation. 28 | 29 | .. toctree:: 30 | :caption: Packages 31 | :maxdepth: 1 32 | 33 | fluent.syntax 34 | fluent.runtime 35 | fluent.pygments 36 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | Sphinx==3.5.1 2 | fluent.pygments==1.0 3 | wheel 4 | -------------------------------------------------------------------------------- /fluent.docs/README.rst: -------------------------------------------------------------------------------- 1 | ``fluent.docs`` 2 | =============== 3 | 4 | Python utilities used by the ``python-fluent`` documentation build 5 | process. The entry point is ``scripts/build-docs``. 6 | 7 | The generated documentation is in ``_build/python-fluent``, and you 8 | can surf it locally via ``python3 -m http.server `` in ``_build``. 9 | 10 | The documentation is created for each tagged version after May 2020, 11 | at which point we had good docs. The current branch (PR tips or 12 | main) is versioned as *dev*, and *stable* is a symlink to the latest 13 | release. The releases are in a dir with their corresponding version number. 14 | 15 | When cutting a new release, manually run the documentation workflow 16 | to pick that up. 17 | 18 | The rationale for regenerating all historic documentation releases is 19 | to provide an easy process to change style and renderering across 20 | the full documentation. For that, the build setup is taken from 21 | the current checkout, and release docs are generated from sources in 22 | a git worktree. 23 | -------------------------------------------------------------------------------- /fluent.docs/fluent/docs/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from .build import DocBuilder 4 | 5 | 6 | def finalize_builddir(repo_name): 7 | "Bookkeeping on the docs build directory" 8 | root = Path("_build") / repo_name 9 | with open(root / ".nojekyll", "w") as fh: 10 | fh.write("") 11 | 12 | 13 | def build_root(repo_name): 14 | """Build the top-level documentation. 15 | 16 | See :py:mod:`.build` on building sub-projects. 17 | """ 18 | with DocBuilder(repo_name, ".") as builder: 19 | builder.build() 20 | -------------------------------------------------------------------------------- /fluent.docs/fluent/docs/tags.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from datetime import date 3 | 4 | 5 | def get_tag_infos(cut_off_date): 6 | """Get fluent.* tags newer than cut_off_date. 7 | TagInfo objects are ordered by committer date, newest first. 8 | """ 9 | taglines = subprocess.run( 10 | [ 11 | "git", 12 | "tag", 13 | "--list", 14 | "fluent.*", 15 | "--sort=-committerdate", 16 | "--format=%(refname:lstrip=2) %(committerdate:short)", 17 | ], 18 | encoding="utf-8", 19 | stdout=subprocess.PIPE, 20 | check=True, 21 | ).stdout.splitlines() 22 | return [ti for ti in (TagInfo(line) for line in taglines) if ti.date > cut_off_date] 23 | 24 | 25 | class TagInfo: 26 | def __init__(self, tagline): 27 | tag, date_string = tagline.split(" ") 28 | self.project, self.version = tag.split("@") 29 | self.date = date.fromisoformat(date_string) 30 | 31 | @property 32 | def tag(self): 33 | return f"{self.project}@{self.version}" 34 | 35 | def __repr__(self): 36 | return f"{self.tag} ({self.date})" 37 | -------------------------------------------------------------------------------- /fluent.docs/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="fluent.docs", 5 | packages=["fluent.docs"], 6 | install_requires=["typing-extensions>=3.7,<5"], 7 | ) 8 | -------------------------------------------------------------------------------- /fluent.pygments/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | fluent.runtime 2.0 (TBD) 5 | ----------------------------------- 6 | 7 | * Drop support for Python 2.7 and 3.5 8 | * Add support for Python 3.6 through 3.9 9 | 10 | fluent.pygments 1.0 (May 20, 2020) 11 | ---------------------------------- 12 | 13 | * Updated documentation to be hosted on https://projectfluent.org/python-fluent/fluent.runtime/. 14 | * Updated package metadata. 15 | -------------------------------------------------------------------------------- /fluent.pygments/README.rst: -------------------------------------------------------------------------------- 1 | fluent.pygments 2 | =============== 3 | 4 | A plugin for pygments to add `Fluent`_ syntax highlighting to Sphinx. 5 | 6 | Installation 7 | ------------ 8 | 9 | .. code-block:: bash 10 | 11 | pip install fluent.pygments 12 | 13 | 14 | Usage 15 | ----- 16 | 17 | .. code-block:: rst 18 | 19 | 20 | .. code-block:: fluent 21 | 22 | my-message = a localized string 23 | 24 | 25 | The `documentation`_ has an example of Fluent content 26 | highlighted with ``fluent.pygments``. 27 | 28 | .. _fluent: https://projectfluent.org/ 29 | .. _documentation: https://projectfluent.org/python-fluent/fluent.pygments 30 | -------------------------------------------------------------------------------- /fluent.pygments/docs/index.rst: -------------------------------------------------------------------------------- 1 | Fluent Syntax Highlighting 2 | ========================== 3 | 4 | The :py:mod:`fluent.pygments` library is built to do syntax highlighting 5 | for `Fluent`_ files in Sphinx. 6 | 7 | 8 | Installation 9 | ------------ 10 | 11 | .. code-block:: bash 12 | 13 | pip install fluent.pygments 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | .. code-block:: rst 20 | 21 | 22 | .. code-block:: fluent 23 | 24 | my-key = Localize { -brand-name } 25 | 26 | Example 27 | ------- 28 | 29 | .. code-block:: fluent 30 | 31 | ### A resource comment for the whole file 32 | 33 | my-key = Localize { -brand-name } 34 | -brand-name = Fluent 35 | 36 | # $num is the number of strings to localize 37 | plurals = { $num -> 38 | [one] One string 39 | *[other] {$num} strings 40 | } 41 | an error ;-) 42 | Most-strings = are just simple strings. 43 | 44 | .. _fluent: https://projectfluent.org/ 45 | -------------------------------------------------------------------------------- /fluent.pygments/fluent/pygments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.pygments/fluent/pygments/__init__.py -------------------------------------------------------------------------------- /fluent.pygments/fluent/pygments/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from fluent.pygments.lexer import FluentLexer 5 | from pygments import highlight 6 | from pygments.formatters import Terminal256Formatter 7 | 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("path") 12 | args = parser.parse_args() 13 | with open(args.path) as fh: 14 | code = fh.read() 15 | highlight(code, FluentLexer(), Terminal256Formatter(), sys.stdout) 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /fluent.pygments/fluent/pygments/lexer.py: -------------------------------------------------------------------------------- 1 | from fluent.syntax import ast as FTL 2 | from fluent.syntax import parse 3 | from pygments.lexer import Lexer 4 | from pygments.token import Token 5 | 6 | 7 | class FluentLexer(Lexer): 8 | name = "Fluent Lexer" 9 | aliases = ["fluent", "ftl"] 10 | filenames = ["*.ftl"] 11 | 12 | def get_tokens_unprocessed(self, text): 13 | last_end = 0 14 | tokenizer = Tokenizer(text) 15 | for token in tokenizer.tokenize(): 16 | node, start, token, span = token 17 | if start > last_end: 18 | yield last_end, Token.Punctuation, text[last_end:start] 19 | last_end = node.span.end 20 | yield start, token, span 21 | if last_end < len(text): 22 | yield last_end, Token.Punctuation, text[last_end:] 23 | 24 | 25 | ATOMIC = { 26 | "Comment": Token.Comment.Multiline, 27 | "GroupComment": Token.Comment.Multiline, 28 | "ResourceComment": Token.Comment.Multiline, 29 | "Identifier": Token.Name.Constant, 30 | "TextElement": Token.Literal, 31 | "NumberLiteral": Token.Literal.Number, 32 | "StringLiteral": Token.Literal.String, 33 | "VariableReference": Token.Name.Variable, 34 | "Junk": Token.Generic.Error, 35 | } 36 | 37 | 38 | class Tokenizer: 39 | def __init__(self, text): 40 | self.text = text 41 | self.ast = parse(text) 42 | 43 | def tokenize(self, node=None): 44 | if node is None: 45 | node = self.ast 46 | if isinstance(node, (FTL.Annotation, FTL.Span)): 47 | return 48 | if isinstance(node, FTL.SyntaxNode): 49 | yield from self.tokenize_node(node) 50 | elif isinstance(node, list): 51 | for child in node: 52 | yield from self.tokenize(child) 53 | 54 | def tokenize_node(self, node): 55 | nodename = type(node).__name__ 56 | if nodename in ATOMIC: 57 | yield self._token(node, ATOMIC[nodename]) 58 | else: 59 | tokenize = getattr(self, f"tokenize_{nodename}", self.generic_tokenize) 60 | yield from tokenize(node) 61 | 62 | def generic_tokenize(self, node): 63 | children = [ 64 | child 65 | for child in vars(node).values() 66 | if isinstance(child, (FTL.SyntaxNode, list)) and child != [] 67 | ] 68 | children.sort( 69 | key=lambda child: ( 70 | child.span.start 71 | if isinstance(child, FTL.SyntaxNode) 72 | else child[0].span.start 73 | ) 74 | ) 75 | for child in children: 76 | yield from self.tokenize(child) 77 | 78 | def tokenize_Variant(self, node): 79 | yield self._token(node.key, Token.Name.Attribute) 80 | yield from self.tokenize(node.value) 81 | 82 | def _token(self, node, token): 83 | return ( 84 | node, 85 | node.span.start, 86 | token, 87 | self.text[node.span.start : node.span.end], 88 | ) 89 | -------------------------------------------------------------------------------- /fluent.pygments/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version=2.0 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [options] 8 | install_requires = 9 | pygments 10 | fluent.syntax 11 | 12 | [options.entry_points] 13 | pygments.lexers = 14 | fluent=fluent.pygments.lexer:FluentLexer 15 | -------------------------------------------------------------------------------- /fluent.pygments/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_namespace_packages, setup 4 | 5 | this_directory = os.path.abspath(os.path.dirname(__file__)) 6 | with open(os.path.join(this_directory, "README.rst"), "rb") as f: 7 | long_description = f.read().decode("utf-8") 8 | 9 | setup( 10 | name="fluent.pygments", 11 | description="Pygments lexer for Fluent.", 12 | long_description=long_description, 13 | long_description_content_type="text/x-rst", 14 | author="Mozilla", 15 | author_email="l10n-drivers@mozilla.org", 16 | license="APL 2", 17 | url="https://github.com/projectfluent/python-fluent", 18 | keywords=["fluent", "pygments"], 19 | classifiers=[ 20 | "Development Status :: 3 - Alpha", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3 :: Only", 28 | ], 29 | packages=find_namespace_packages(include=["fluent.*"]), 30 | test_suite="tests.pygments", 31 | ) 32 | -------------------------------------------------------------------------------- /fluent.pygments/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.pygments/tests/__init__.py -------------------------------------------------------------------------------- /fluent.pygments/tests/pygments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.pygments/tests/pygments/__init__.py -------------------------------------------------------------------------------- /fluent.pygments/tests/pygments/test_lexer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from fluent.pygments.lexer import FluentLexer 4 | from pygments.token import Token 5 | 6 | 7 | class LexerTest(unittest.TestCase): 8 | def setUp(self): 9 | self.lexer = FluentLexer() 10 | 11 | def test_comment(self): 12 | fragment = "# comment\n" 13 | tokens = [ 14 | (Token.Comment.Multiline, "# comment"), 15 | (Token.Punctuation, "\n"), 16 | ] 17 | self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) 18 | 19 | def test_message(self): 20 | fragment = "msg = some value\n" 21 | tokens = [ 22 | (Token.Name.Constant, "msg"), 23 | (Token.Punctuation, " = "), 24 | (Token.Literal, "some value"), 25 | (Token.Punctuation, "\n"), 26 | ] 27 | self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) 28 | 29 | def test_message_with_comment(self): 30 | fragment = "# good comment\nmsg = some value\n" 31 | tokens = [ 32 | (Token.Comment.Multiline, "# good comment"), 33 | (Token.Punctuation, "\n"), 34 | (Token.Name.Constant, "msg"), 35 | (Token.Punctuation, " = "), 36 | (Token.Literal, "some value"), 37 | (Token.Punctuation, "\n"), 38 | ] 39 | self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) 40 | -------------------------------------------------------------------------------- /fluent.runtime/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | fluent.runtime 0.4.0 (March 13, 2023) 5 | ------------------------------------- 6 | 7 | * Drop support for Python 2.7 and 3.5 & support for Python 3.6 through 3.9 ([#163](https://github.com/projectfluent/python-fluent/pull/163)) 8 | * Add type hints ([#180](https://github.com/projectfluent/python-fluent/pull/180)) 9 | 10 | fluent.runtime 0.3.1 (May 20, 2020) 11 | ----------------------------------- 12 | 13 | * Updated documentation to be hosted on https://projectfluent.org/python-fluent/fluent.runtime/. 14 | * Updated package metadata. 15 | 16 | fluent.runtime 0.3 (October 23, 2019) 17 | ------------------------------------- 18 | 19 | * Added ``fluent.runtime.FluentResource`` and 20 | ``fluent.runtime.FluentBundle.add_resource``. 21 | * Removed ``fluent.runtime.FluentBundle.add_messages``. 22 | * Replaced ``bundle.format()`` with ``bundle.format_pattern(bundle.get_message().value)``. 23 | * Added ``fluent.runtime.FluentLocalization`` as main entrypoint for applications. 24 | 25 | fluent.runtime 0.2 (September 10, 2019) 26 | --------------------------------------- 27 | 28 | * Support for Fluent spec 1.0 (``fluent.syntax`` 0.17), including parameterized 29 | terms. 30 | 31 | fluent.runtime 0.1 (January 21, 2019) 32 | ------------------------------------- 33 | 34 | First release to PyPI of ``fluent.runtime``. This release contains a 35 | ``FluentBundle`` implementation that can generate translations from FTL 36 | messages. It targets the `Fluent 0.7 spec 37 | `_. 38 | -------------------------------------------------------------------------------- /fluent.runtime/README.rst: -------------------------------------------------------------------------------- 1 | fluent.runtime |fluent.runtime| 2 | =============================== 3 | 4 | Use `Fluent`_ to localize your Python application. It comes with a ``Localization`` 5 | class to use, based on an implementation of ``FluentBundle``. It uses the parser from 6 | ``fluent.syntax`` to read Fluent files. 7 | 8 | .. code-block:: python 9 | 10 | >>> from datetime import date 11 | >>> l10n = DemoLocalization("today-is = Today is { $today }") 12 | >>> val = l10n.format_value("today-is", {"today": date.today() }) 13 | >>> val 14 | 'Today is Jun 16, 2018' 15 | 16 | Find the full documentation on https://projectfluent.org/python-fluent/fluent.runtime/. 17 | 18 | .. _fluent: https://projectfluent.org/ 19 | .. |fluent.runtime| image:: https://github.com/projectfluent/python-fluent/workflows/fluent.runtime/badge.svg 20 | -------------------------------------------------------------------------------- /fluent.runtime/docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /fluent.runtime/docs/index.rst: -------------------------------------------------------------------------------- 1 | .. fluent.runtime documentation master file, created by 2 | sphinx-quickstart on Thu Jan 24 15:45:23 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to fluent.runtime's documentation! 7 | ========================================== 8 | 9 | These are the docs for ``fluent.runtime`` |release|. Check the :doc:`/history` for 10 | significant changes. 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | :caption: Contents: 15 | 16 | installation 17 | usage 18 | internals 19 | reference 20 | history 21 | -------------------------------------------------------------------------------- /fluent.runtime/docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | ``fluent.runtime`` can be installed with pip:: 6 | 7 | $ pip install fluent.runtime 8 | 9 | Python 2.7 or 3.5+ required. Earlier versions of Python 3 may work but they are 10 | not officially supported. 11 | -------------------------------------------------------------------------------- /fluent.runtime/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /fluent.runtime/docs/reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: fluent.runtime 5 | :members: 6 | -------------------------------------------------------------------------------- /fluent.runtime/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | fluent.pygments 2 | -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/__init__.py: -------------------------------------------------------------------------------- 1 | from fluent.syntax import FluentParser 2 | from fluent.syntax.ast import Resource 3 | 4 | from .bundle import FluentBundle 5 | from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader 6 | 7 | __all__ = [ 8 | "FluentLocalization", 9 | "AbstractResourceLoader", 10 | "FluentResourceLoader", 11 | "FluentResource", 12 | "FluentBundle", 13 | ] 14 | 15 | 16 | def FluentResource(source: str) -> Resource: 17 | parser = FluentParser() 18 | return parser.parse(source) 19 | -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/builtins.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict 2 | 3 | from .types import FluentType, fluent_date, fluent_number 4 | 5 | NUMBER = fluent_number 6 | DATETIME = fluent_date 7 | 8 | 9 | BUILTINS: Dict[str, Callable[[Any], FluentType]] = { 10 | "NUMBER": NUMBER, 11 | "DATETIME": DATETIME, 12 | } 13 | -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/errors.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | 4 | class FluentFormatError(ValueError): 5 | def __eq__(self, other: object) -> bool: 6 | return (other.__class__ == self.__class__) and cast( 7 | ValueError, other 8 | ).args == self.args 9 | 10 | 11 | class FluentReferenceError(FluentFormatError): 12 | pass 13 | 14 | 15 | class FluentCyclicReferenceError(FluentFormatError): 16 | pass 17 | -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/prepare.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from fluent.syntax import ast as FTL 4 | 5 | from . import resolver 6 | 7 | 8 | class Compiler: 9 | def __call__(self, item: Any) -> Any: 10 | if isinstance(item, FTL.BaseNode): 11 | return self.compile(item) 12 | if isinstance(item, (tuple, list)): 13 | return [self(elem) for elem in item] 14 | return item 15 | 16 | def compile(self, node: Any) -> Any: 17 | nodename: str = type(node).__name__ 18 | if not hasattr(resolver, nodename): 19 | return node 20 | kwargs: Dict[str, Any] = vars(node).copy() 21 | for propname, propvalue in kwargs.items(): 22 | kwargs[propname] = self(propvalue) 23 | handler = getattr(self, "compile_" + nodename, self.compile_generic) 24 | return handler(nodename, **kwargs) 25 | 26 | def compile_generic(self, nodename: str, **kwargs: Any) -> Any: 27 | return getattr(resolver, nodename)(**kwargs) 28 | 29 | def compile_Placeable(self, _: Any, expression: Any, **kwargs: Any) -> Any: 30 | if isinstance(expression, resolver.Literal): 31 | return expression 32 | return resolver.Placeable(expression=expression, **kwargs) 33 | 34 | def compile_Pattern(self, _: Any, elements: List[Any], **kwargs: Any) -> Any: 35 | if len(elements) == 1 and isinstance(elements[0], resolver.Placeable): 36 | # Don't isolate isolated placeables 37 | return resolver.NeverIsolatingPlaceable(elements[0].expression) 38 | if any(not isinstance(child, resolver.Literal) for child in elements): 39 | return resolver.Pattern(elements=elements, **kwargs) 40 | if len(elements) == 1: 41 | return elements[0] 42 | return resolver.TextElement("".join(child(None) for child in elements)) 43 | -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.runtime/fluent/runtime/py.typed -------------------------------------------------------------------------------- /fluent.runtime/fluent/runtime/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | from decimal import Decimal 3 | from typing import Any, Union 4 | 5 | from fluent.syntax.ast import MessageReference, TermReference 6 | 7 | from .errors import FluentReferenceError 8 | from .types import FluentDate, FluentDateTime, FluentDecimal, FluentFloat, FluentInt 9 | 10 | TERM_SIGIL = "-" 11 | ATTRIBUTE_SEPARATOR = "." 12 | 13 | 14 | def native_to_fluent(val: Any) -> Any: 15 | """ 16 | Convert a python type to a Fluent Type. 17 | """ 18 | if isinstance(val, int): 19 | return FluentInt(val) 20 | if isinstance(val, float): 21 | return FluentFloat(val) 22 | if isinstance(val, Decimal): 23 | return FluentDecimal(val) 24 | 25 | if isinstance(val, datetime): 26 | return FluentDateTime.from_date_time(val) 27 | if isinstance(val, date): 28 | return FluentDate.from_date(val) 29 | return val 30 | 31 | 32 | def reference_to_id(ref: Union[MessageReference, TermReference]) -> str: 33 | """ 34 | Returns a string reference for a MessageReference or TermReference 35 | AST node. 36 | 37 | e.g. 38 | message 39 | message.attr 40 | -term 41 | -term.attr 42 | """ 43 | start: str 44 | if isinstance(ref, TermReference): 45 | start = TERM_SIGIL + ref.id.name 46 | else: 47 | start = ref.id.name 48 | 49 | if ref.attribute: 50 | return "".join([start, ATTRIBUTE_SEPARATOR, ref.attribute.name]) 51 | return start 52 | 53 | 54 | def unknown_reference_error_obj(ref_id: str) -> FluentReferenceError: 55 | if ATTRIBUTE_SEPARATOR in ref_id: 56 | return FluentReferenceError(f"Unknown attribute: {ref_id}") 57 | if ref_id.startswith(TERM_SIGIL): 58 | return FluentReferenceError(f"Unknown term: {ref_id}") 59 | return FluentReferenceError(f"Unknown message: {ref_id}") 60 | -------------------------------------------------------------------------------- /fluent.runtime/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version=0.4.0 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /fluent.runtime/setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import setup 4 | 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, "README.rst"), "rb") as f: 7 | long_description = f.read().decode("utf-8") 8 | 9 | 10 | setup( 11 | name="fluent.runtime", 12 | description="Localization library for expressive translations.", 13 | long_description=long_description, 14 | long_description_content_type="text/x-rst", 15 | author="Luke Plant", 16 | author_email="L.Plant.98@cantab.net", 17 | license="APL 2", 18 | url="https://github.com/projectfluent/python-fluent", 19 | keywords=["fluent", "localization", "l10n"], 20 | classifiers=[ 21 | "Development Status :: 3 - Alpha", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3 :: Only", 29 | ], 30 | packages=["fluent.runtime"], 31 | package_data={"fluent.runtime": ["py.typed"]}, 32 | # These should also be duplicated in tox.ini and /.github/workflows/fluent.runtime.yml 33 | python_requires=">=3.6", 34 | install_requires=[ 35 | "fluent.syntax>=0.17,<0.20", 36 | "attrs", 37 | "babel", 38 | "pytz", 39 | "typing-extensions>=3.7,<5", 40 | ], 41 | test_suite="tests", 42 | ) 43 | -------------------------------------------------------------------------------- /fluent.runtime/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.runtime/tests/__init__.py -------------------------------------------------------------------------------- /fluent.runtime/tests/format/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.runtime/tests/format/__init__.py -------------------------------------------------------------------------------- /fluent.runtime/tests/format/test_arguments.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from fluent.runtime import FluentBundle, FluentResource 4 | 5 | from ..utils import dedent_ftl 6 | 7 | 8 | class TestNumbersInValues(unittest.TestCase): 9 | def setUp(self): 10 | self.bundle = FluentBundle(["en-US"], use_isolating=False) 11 | self.bundle.add_resource( 12 | FluentResource( 13 | dedent_ftl( 14 | """ 15 | foo = Foo { $num } 16 | bar = { foo } 17 | baz = 18 | .attr = Baz Attribute { $num } 19 | qux = { "a" -> 20 | *[a] Baz Variant A { $num } 21 | } 22 | """ 23 | ) 24 | ) 25 | ) 26 | 27 | def test_can_be_used_in_the_message_value(self): 28 | val, errs = self.bundle.format_pattern( 29 | self.bundle.get_message("foo").value, {"num": 3} 30 | ) 31 | self.assertEqual(val, "Foo 3") 32 | self.assertEqual(len(errs), 0) 33 | 34 | def test_can_be_used_in_the_message_value_which_is_referenced(self): 35 | val, errs = self.bundle.format_pattern( 36 | self.bundle.get_message("bar").value, {"num": 3} 37 | ) 38 | self.assertEqual(val, "Foo 3") 39 | self.assertEqual(len(errs), 0) 40 | 41 | def test_can_be_used_in_an_attribute(self): 42 | val, errs = self.bundle.format_pattern( 43 | self.bundle.get_message("baz").attributes["attr"], {"num": 3} 44 | ) 45 | self.assertEqual(val, "Baz Attribute 3") 46 | self.assertEqual(len(errs), 0) 47 | 48 | def test_can_be_used_in_a_variant(self): 49 | val, errs = self.bundle.format_pattern( 50 | self.bundle.get_message("qux").value, {"num": 3} 51 | ) 52 | self.assertEqual(val, "Baz Variant A 3") 53 | self.assertEqual(len(errs), 0) 54 | 55 | 56 | class TestStrings(unittest.TestCase): 57 | def setUp(self): 58 | self.bundle = FluentBundle(["en-US"], use_isolating=False) 59 | self.bundle.add_resource( 60 | FluentResource( 61 | dedent_ftl( 62 | """ 63 | foo = { $arg } 64 | """ 65 | ) 66 | ) 67 | ) 68 | 69 | def test_can_be_a_string(self): 70 | val, errs = self.bundle.format_pattern( 71 | self.bundle.get_message("foo").value, {"arg": "Argument"} 72 | ) 73 | self.assertEqual(val, "Argument") 74 | self.assertEqual(len(errs), 0) 75 | -------------------------------------------------------------------------------- /fluent.runtime/tests/format/test_isolating.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from fluent.runtime import FluentBundle, FluentResource 4 | 5 | from ..utils import dedent_ftl 6 | 7 | # Unicode bidi isolation characters. 8 | FSI = "\u2068" 9 | PDI = "\u2069" 10 | 11 | 12 | class TestUseIsolating(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.bundle = FluentBundle(["en-US"]) 16 | self.bundle.add_resource( 17 | FluentResource( 18 | dedent_ftl( 19 | """ 20 | foo = Foo 21 | bar = { foo } Bar 22 | baz = { $arg } Baz 23 | qux = { bar } { baz } 24 | """ 25 | ) 26 | ) 27 | ) 28 | 29 | def test_isolates_interpolated_message_references(self): 30 | val, errs = self.bundle.format_pattern(self.bundle.get_message("bar").value, {}) 31 | self.assertEqual(val, FSI + "Foo" + PDI + " Bar") 32 | self.assertEqual(len(errs), 0) 33 | 34 | def test_isolates_interpolated_string_typed_variable_references(self): 35 | val, errs = self.bundle.format_pattern( 36 | self.bundle.get_message("baz").value, {"arg": "Arg"} 37 | ) 38 | self.assertEqual(val, FSI + "Arg" + PDI + " Baz") 39 | self.assertEqual(len(errs), 0) 40 | 41 | def test_isolates_interpolated_number_typed_variable_references(self): 42 | val, errs = self.bundle.format_pattern( 43 | self.bundle.get_message("baz").value, {"arg": 1} 44 | ) 45 | self.assertEqual(val, FSI + "1" + PDI + " Baz") 46 | self.assertEqual(len(errs), 0) 47 | 48 | def test_isolates_complex_interpolations(self): 49 | val, errs = self.bundle.format_pattern( 50 | self.bundle.get_message("qux").value, {"arg": "Arg"} 51 | ) 52 | expected_bar = FSI + FSI + "Foo" + PDI + " Bar" + PDI 53 | expected_baz = FSI + FSI + "Arg" + PDI + " Baz" + PDI 54 | self.assertEqual(val, expected_bar + " " + expected_baz) 55 | self.assertEqual(len(errs), 0) 56 | 57 | 58 | class TestSkipIsolating(unittest.TestCase): 59 | 60 | def setUp(self): 61 | self.bundle = FluentBundle(["en-US"]) 62 | self.bundle.add_resource( 63 | FluentResource( 64 | dedent_ftl( 65 | """ 66 | -brand-short-name = Amaya 67 | foo = { -brand-short-name } 68 | with-arg = { $arg } 69 | """ 70 | ) 71 | ) 72 | ) 73 | 74 | def test_skip_isolating_chars_if_just_one_message_ref(self): 75 | val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) 76 | self.assertEqual(val, "Amaya") 77 | self.assertEqual(len(errs), 0) 78 | 79 | def test_skip_isolating_chars_if_just_one_placeable_arg(self): 80 | val, errs = self.bundle.format_pattern( 81 | self.bundle.get_message("with-arg").value, {"arg": "Arg"} 82 | ) 83 | self.assertEqual(val, "Arg") 84 | self.assertEqual(len(errs), 0) 85 | -------------------------------------------------------------------------------- /fluent.runtime/tests/test_bomb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from fluent.runtime import FluentBundle, FluentResource 4 | 5 | from .utils import dedent_ftl 6 | 7 | 8 | class TestBillionLaughs(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.ctx = FluentBundle(["en-US"], use_isolating=False) 12 | self.ctx.add_resource( 13 | FluentResource( 14 | dedent_ftl( 15 | """ 16 | lol0 = 01234567890123456789012345678901234567890123456789 17 | lol1 = {lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0} 18 | lol2 = {lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1} 19 | lol3 = {lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2} 20 | lol4 = {lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3} 21 | lolz = {lol4} 22 | 23 | elol0 = { "" } 24 | elol1 = {elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0} 25 | elol2 = {elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1} 26 | elol3 = {elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2} 27 | elol4 = {elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3} 28 | elol5 = {elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4} 29 | elol6 = {elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5} 30 | emptylolz = {elol6} 31 | 32 | """ 33 | ) 34 | ) 35 | ) 36 | 37 | def test_max_length_protection(self): 38 | val, errs = self.ctx.format_pattern(self.ctx.get_message("lolz").value) 39 | self.assertEqual(val, "{???}") 40 | self.assertNotEqual(len(errs), 0) 41 | self.assertIn("Too many characters", str(errs[-1])) 42 | 43 | def test_max_expansions_protection(self): 44 | # Without protection, emptylolz will take a really long time to 45 | # evaluate, although it generates an empty message. 46 | val, errs = self.ctx.format_pattern(self.ctx.get_message("emptylolz").value) 47 | self.assertEqual(val, "{???}") 48 | self.assertEqual(len(errs), 1) 49 | self.assertIn("Too many parts", str(errs[-1])) 50 | -------------------------------------------------------------------------------- /fluent.runtime/tests/test_fallback.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .utils import patch_files 3 | 4 | from fluent.runtime import FluentLocalization, FluentResourceLoader 5 | 6 | 7 | class TestLocalization(unittest.TestCase): 8 | def test_init(self): 9 | l10n = FluentLocalization( 10 | ["en"], ["file.ftl"], FluentResourceLoader("{locale}") 11 | ) 12 | self.assertTrue(callable(l10n.format_value)) 13 | 14 | @patch_files({ 15 | "de/one.ftl": "one = in German", 16 | "de/two.ftl": "two = in German", 17 | "fr/two.ftl": "three = in French", 18 | "en/one.ftl": "four = exists", 19 | "en/two.ftl": "five = exists", 20 | }) 21 | def test_bundles(self): 22 | l10n = FluentLocalization( 23 | ["de", "fr", "en"], ["one.ftl", "two.ftl"], FluentResourceLoader("{locale}") 24 | ) 25 | bundles_gen = l10n._bundles() 26 | bundle_de = next(bundles_gen) 27 | self.assertEqual(bundle_de.locales[0], "de") 28 | self.assertTrue(bundle_de.has_message("one")) 29 | self.assertTrue(bundle_de.has_message("two")) 30 | bundle_fr = next(bundles_gen) 31 | self.assertEqual(bundle_fr.locales[0], "fr") 32 | self.assertFalse(bundle_fr.has_message("one")) 33 | self.assertTrue(bundle_fr.has_message("three")) 34 | self.assertListEqual(list(l10n._bundles())[:2], [bundle_de, bundle_fr]) 35 | bundle_en = next(bundles_gen) 36 | self.assertEqual(bundle_en.locales[0], "en") 37 | self.assertEqual(l10n.format_value("one"), "in German") 38 | self.assertEqual(l10n.format_value("two"), "in German") 39 | self.assertEqual(l10n.format_value("three"), "in French") 40 | self.assertEqual(l10n.format_value("four"), "exists") 41 | self.assertEqual(l10n.format_value("five"), "exists") 42 | 43 | 44 | class TestResourceLoader(unittest.TestCase): 45 | @patch_files({ 46 | "en/one.ftl": "one = exists", 47 | "en/two.ftl": "two = exists", 48 | }) 49 | def test_all_exist(self): 50 | loader = FluentResourceLoader("{locale}") 51 | resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) 52 | self.assertEqual(len(resources_list), 1) 53 | resources = resources_list[0] 54 | self.assertEqual(len(resources), 2) 55 | 56 | @patch_files({ 57 | "en/two.ftl": "two = exists", 58 | }) 59 | def test_one_exists(self): 60 | loader = FluentResourceLoader("{locale}") 61 | resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) 62 | self.assertEqual(len(resources_list), 1) 63 | resources = resources_list[0] 64 | self.assertEqual(len(resources), 1) 65 | 66 | @patch_files({}) 67 | def test_none_exist(self): 68 | loader = FluentResourceLoader("{locale}") 69 | resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) 70 | self.assertEqual(len(resources_list), 0) 71 | -------------------------------------------------------------------------------- /fluent.runtime/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .utils import patch_files 3 | import os 4 | import codecs 5 | 6 | 7 | class TestFileSimulate(unittest.TestCase): 8 | def test_basic(self): 9 | @patch_files({ 10 | "the.txt": "The", 11 | "en/one.txt": "One", 12 | "en/two.txt": "Two" 13 | }) 14 | def patch_me(a, b): 15 | self.assertEqual(a, 10) 16 | self.assertEqual(b, "b") 17 | self.assertFileIs(os.path.basename(__file__), None) 18 | self.assertFileIs("the.txt", "The") 19 | self.assertFileIs("en/one.txt", "One") 20 | self.assertFileIs("en\\one.txt", "One") 21 | self.assertFileIs("en/two.txt", "Two") 22 | self.assertFileIs("en\\two.txt", "Two") 23 | self.assertFileIs("en/three.txt", None) 24 | self.assertFileIs("en\\three.txt", None) 25 | 26 | with self.assertRaises(ValueError): 27 | os.path.isfile("en/") 28 | patch_me(10, "b") 29 | 30 | def assertFileIs(self, filename, expect_contents): 31 | """ 32 | expect_contents is None: Expect file does not exist 33 | expect_contents is a str: Expect file to exist and contents to match 34 | """ 35 | if expect_contents is None: 36 | self.assertFalse(os.path.isfile(filename), 37 | f"Expected {filename} to not exist.") 38 | else: 39 | self.assertTrue(os.path.isfile(filename), 40 | f"Expected {filename} to exist.") 41 | with codecs.open(filename, "r", "utf-8") as f: 42 | self.assertEqual(f.read(), expect_contents) 43 | -------------------------------------------------------------------------------- /fluent.runtime/tests/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for testing.""" 2 | 3 | import textwrap 4 | from pathlib import PureWindowsPath, PurePosixPath 5 | from unittest import mock 6 | from io import StringIO 7 | import functools 8 | 9 | 10 | def dedent_ftl(text): 11 | return textwrap.dedent(f"{text.rstrip()}\n") 12 | 13 | 14 | # Needed in test_falllback.py because it uses dict + string compare to make a virtual file structure 15 | def _normalize_file_path(path): 16 | """Note: Does not support absolute paths or paths that 17 | contain '.' or '..' parts.""" 18 | # Cannot use os.path or PurePath, because they only recognize 19 | # one kind of path separator 20 | if PureWindowsPath(path).is_absolute() or PurePosixPath(path).is_absolute(): 21 | raise ValueError(f"Unsupported path: {path}") 22 | parts = path.replace("\\", "/").split("/") 23 | if "." in parts or ".." in parts: 24 | raise ValueError(f"Unsupported path: {path}") 25 | if parts and parts[-1] == "": 26 | # path ends with a trailing pathsep 27 | raise ValueError(f"Path appears to be a directory, not a file: {path}") 28 | return "/".join(parts) 29 | 30 | 31 | def patch_files(files: dict): 32 | """Decorate a function to simulate files ``files`` during the function. 33 | 34 | The keys of ``files`` are file names and must use '/' for path separator. 35 | The values are file contents. Directories or relative paths are not supported. 36 | Example: ``{"en/one.txt": "One", "en/two.txt": "Two"}`` 37 | 38 | The implementation may be changed to match the mechanism used. 39 | """ 40 | 41 | # Here it is possible to validate file names, but skipped 42 | 43 | def then(func): 44 | @mock.patch("os.path.isfile", side_effect=lambda p: _normalize_file_path(p) in files) 45 | @mock.patch("codecs.open", side_effect=lambda p, _, __: StringIO(files[_normalize_file_path(p)])) 46 | @functools.wraps(func) # Make ret look like func to later decorators 47 | def ret(*args, **kwargs): 48 | func(*args[:-2], **kwargs) 49 | return ret 50 | return then 51 | -------------------------------------------------------------------------------- /fluent.runtime/tools/benchmarks/README.md: -------------------------------------------------------------------------------- 1 | To run the benchmarks, do: 2 | 3 | $ pip install -r tools/benchmarks/requirements.txt 4 | $ py.test ./tools/benchmarks/fluent_benchmark.py::TestBenchmark --benchmark-warmup=on 5 | 6 | To profile the benchmark suite, we recommend py-spy as a 7 | good tool. Install py-spy: https://github.com/benfred/py-spy 8 | 9 | Then do something like this to profile the benchmark. Depending on your 10 | platform, you might need to use `sudo`. 11 | 12 | $ py-spy -f prof.svg -- py.test ./tools/benchmarks/fluent_benchmark.py::TestBenchmark --benchmark-warmup=off 13 | 14 | And look at prof.svg in a browser. Note that this diagram includes the fixture 15 | setup, warmup and calibration phases which you should ignore. 16 | -------------------------------------------------------------------------------- /fluent.runtime/tools/benchmarks/fluent_benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This should be run using pytest 3 | 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | 8 | import pytest 9 | from fluent.runtime import FluentBundle, FluentResource 10 | 11 | FTL_CONTENT = """ 12 | one = One 13 | two = Two 14 | three = Three 15 | four = Four 16 | five = Five 17 | six = Six 18 | seven = Seven ways to { $destination } 19 | eight = Eight 20 | nine = Nine 21 | ten = Ten 22 | """ 23 | 24 | 25 | @pytest.fixture 26 | def fluent_bundle(): 27 | bundle = FluentBundle(["pl"], use_isolating=False) 28 | bundle.add_resource(FluentResource(FTL_CONTENT)) 29 | return bundle 30 | 31 | 32 | def fluent_template(bundle): 33 | return ( 34 | "preface" 35 | + bundle.format_pattern(bundle.get_message("one").value)[0] 36 | + bundle.format_pattern(bundle.get_message("two").value)[0] 37 | + bundle.format_pattern(bundle.get_message("three").value)[0] 38 | + bundle.format_pattern(bundle.get_message("four").value)[0] 39 | + bundle.format_pattern(bundle.get_message("five").value)[0] 40 | + bundle.format_pattern(bundle.get_message("six").value)[0] 41 | + bundle.format_pattern( 42 | bundle.get_message("seven").value, {"destination": "Mars"} 43 | )[0] 44 | + bundle.format_pattern(bundle.get_message("eight").value)[0] 45 | + bundle.format_pattern(bundle.get_message("nine").value)[0] 46 | + bundle.format_pattern(bundle.get_message("ten").value)[0] 47 | + "tail" 48 | ) 49 | 50 | 51 | class TestBenchmark(object): 52 | def test_template(self, fluent_bundle, benchmark): 53 | benchmark(lambda: fluent_template(fluent_bundle)) 54 | 55 | def test_bundle(self, benchmark): 56 | def test_bundles(): 57 | FluentBundle(["pl"], use_isolating=False) 58 | FluentBundle(["fr"], use_isolating=False) 59 | 60 | benchmark(test_bundles) 61 | 62 | def test_import(self, benchmark): 63 | def test_imports(): 64 | # prune cached imports 65 | fluent_deps = [ 66 | k 67 | for k in sys.modules.keys() 68 | if k.split(".", 1)[0] in ("babel", "fluent", "pytz") 69 | ] 70 | for k in fluent_deps: 71 | del sys.modules[k] 72 | from fluent.runtime import FluentBundle # noqa 73 | 74 | benchmark(test_imports) 75 | -------------------------------------------------------------------------------- /fluent.runtime/tools/benchmarks/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-benchmark 3 | -------------------------------------------------------------------------------- /fluent.runtime/tox.ini: -------------------------------------------------------------------------------- 1 | # This config is for local testing. 2 | # It should be correspond to .github/workflows/fluent.runtime.yml 3 | [tox] 4 | envlist = {py36,py37,py38,py39,pypy3}-syntax, py3-syntax0.17, latest 5 | skipsdist=True 6 | 7 | [testenv] 8 | setenv = 9 | PYTHONPATH = {toxinidir} 10 | deps = 11 | syntax0.17: fluent.syntax==0.17 12 | attrs==19.1.0 13 | babel==2.7.0 14 | pytz==2019.2 15 | typing-extensions~=3.7 16 | syntax: . 17 | commands = python -m unittest 18 | 19 | [testenv:latest] 20 | basepython = python3 21 | deps = 22 | . 23 | -------------------------------------------------------------------------------- /fluent.syntax/.gitattributes: -------------------------------------------------------------------------------- 1 | tests/syntax/fixtures_reference/crlf.ftl eol=crlf 2 | tests/syntax/fixtures_reference/cr.ftl eol=cr 3 | tests/syntax/fixtures_structure/crlf.ftl eol=crlf 4 | -------------------------------------------------------------------------------- /fluent.syntax/README.rst: -------------------------------------------------------------------------------- 1 | ``fluent.syntax`` |fluent.syntax| 2 | --------------------------------- 3 | 4 | Read, write, and transform `Fluent`_ files. 5 | 6 | This package includes the parser, serializer, and traversal 7 | utilities like Visitor and Transformer. You’re looking for this package 8 | if you work on tooling for Fluent in Python. 9 | 10 | .. code-block:: python 11 | 12 | >>> from fluent.syntax import parse, ast, serialize 13 | >>> resource = parse("a-key = String to localize") 14 | >>> resource.body[0].value.elements[0].value = "Localized string" 15 | >>> serialize(resource) 16 | 'a-key = Localized string\n' 17 | 18 | 19 | Find the full documentation on https://projectfluent.org/python-fluent/fluent.syntax/. 20 | 21 | .. _fluent: https://projectfluent.org/ 22 | .. |fluent.syntax| image:: https://github.com/projectfluent/python-fluent/workflows/fluent.syntax/badge.svg 23 | -------------------------------------------------------------------------------- /fluent.syntax/docs/ast.rst: -------------------------------------------------------------------------------- 1 | AST 2 | === 3 | 4 | 5 | .. automodule:: fluent.syntax.ast 6 | :members: 7 | :exclude-members: scalars_equal 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /fluent.syntax/docs/index.rst: -------------------------------------------------------------------------------- 1 | Fluent Syntax 2 | ============= 3 | 4 | The :py:mod:`fluent.syntax` library is built for tooling around Fluent. It's 5 | designed to parse, analyze, process, and serialize Fluent files. 6 | 7 | The :py:mod:`fluent.syntax.ast` module implements ``Visitor`` and ``Transformer`` 8 | patterns, which are the recommended interfaces to be used in analysis and 9 | post-processing of Fluent source files. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | usage 16 | reference 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /fluent.syntax/docs/parsing.rst: -------------------------------------------------------------------------------- 1 | Parsing 2 | ======= 3 | 4 | .. py:module:: fluent.syntax.parser 5 | 6 | .. autoclass:: fluent.syntax.parser.FluentParser 7 | :members: parse, parse_entry 8 | -------------------------------------------------------------------------------- /fluent.syntax/docs/reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | The :py:mod:`fluent.syntax` module contains two helper methods. 5 | 6 | The :py:mod:`fluent.syntax.parser` and :py:mod:`fluent.syntax.serializer` modules 7 | provide more fine-grained control and detail. 8 | 9 | 10 | .. automodule:: fluent.syntax 11 | :members: 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Contents: 17 | 18 | parsing 19 | ast 20 | visitor 21 | serializing 22 | -------------------------------------------------------------------------------- /fluent.syntax/docs/serializing.rst: -------------------------------------------------------------------------------- 1 | Serializing 2 | =========== 3 | 4 | .. automodule:: fluent.syntax.serializer 5 | :members: 6 | -------------------------------------------------------------------------------- /fluent.syntax/docs/visitor.rst: -------------------------------------------------------------------------------- 1 | Visitor & Transformer 2 | ===================== 3 | 4 | 5 | .. automodule:: fluent.syntax.visitor 6 | :members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /fluent.syntax/fluent/syntax/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from . import ast 4 | from .errors import ParseError 5 | from .parser import FluentParser 6 | from .serializer import FluentSerializer 7 | from .stream import FluentParserStream 8 | from .visitor import Transformer, Visitor 9 | 10 | __all__ = [ 11 | "FluentParser", 12 | "FluentParserStream", 13 | "FluentSerializer", 14 | "ParseError", 15 | "Transformer", 16 | "Visitor", 17 | "ast", 18 | "parse", 19 | "serialize", 20 | ] 21 | 22 | 23 | def parse(source: str, **kwargs: Any) -> ast.Resource: 24 | """Create an ast.Resource from a Fluent Syntax source.""" 25 | parser = FluentParser(**kwargs) 26 | return parser.parse(source) 27 | 28 | 29 | def serialize(resource: ast.Resource, **kwargs: Any) -> str: 30 | """Serialize an ast.Resource to a unicode string.""" 31 | serializer = FluentSerializer(**kwargs) 32 | return serializer.serialize(resource) 33 | -------------------------------------------------------------------------------- /fluent.syntax/fluent/syntax/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Union 2 | 3 | 4 | class ParseError(Exception): 5 | def __init__(self, code: str, *args: Union[str, None]): 6 | self.code = code 7 | self.args = args 8 | self.message = get_error_message(code, args) 9 | 10 | 11 | def get_error_message(code: str, args: Tuple[Union[str, None], ...]) -> str: 12 | if code == "E00001": 13 | return "Generic error" 14 | if code == "E0002": 15 | return "Expected an entry start" 16 | if code == "E0003": 17 | return 'Expected token: "{}"'.format(args[0]) 18 | if code == "E0004": 19 | return 'Expected a character from range: "{}"'.format(args[0]) 20 | if code == "E0005": 21 | msg = 'Expected message "{}" to have a value or attributes' 22 | return msg.format(args[0]) 23 | if code == "E0006": 24 | msg = 'Expected term "-{}" to have a value' 25 | return msg.format(args[0]) 26 | if code == "E0007": 27 | return "Keyword cannot end with a whitespace" 28 | if code == "E0008": 29 | return "The callee has to be an upper-case identifier or a term" 30 | if code == "E0009": 31 | return "The argument name has to be a simple identifier" 32 | if code == "E0010": 33 | return "Expected one of the variants to be marked as default (*)" 34 | if code == "E0011": 35 | return 'Expected at least one variant after "->"' 36 | if code == "E0012": 37 | return "Expected value" 38 | if code == "E0013": 39 | return "Expected variant key" 40 | if code == "E0014": 41 | return "Expected literal" 42 | if code == "E0015": 43 | return "Only one variant can be marked as default (*)" 44 | if code == "E0016": 45 | return "Message references cannot be used as selectors" 46 | if code == "E0017": 47 | return "Terms cannot be used as selectors" 48 | if code == "E0018": 49 | return "Attributes of messages cannot be used as selectors" 50 | if code == "E0019": 51 | return "Attributes of terms cannot be used as placeables" 52 | if code == "E0020": 53 | return "Unterminated string expression" 54 | if code == "E0021": 55 | return "Positional arguments must not follow named arguments" 56 | if code == "E0022": 57 | return "Named arguments must be unique" 58 | if code == "E0024": 59 | return "Cannot access variants of a message." 60 | if code == "E0025": 61 | return "Unknown escape sequence: \\{}.".format(args[0]) 62 | if code == "E0026": 63 | return "Invalid Unicode escape sequence: {}.".format(args[0]) 64 | if code == "E0027": 65 | return "Unbalanced closing brace in TextElement." 66 | if code == "E0028": 67 | return "Expected an inline expression" 68 | if code == "E0029": 69 | return "Expected simple expression as selector" 70 | return code 71 | -------------------------------------------------------------------------------- /fluent.syntax/fluent/syntax/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.syntax/fluent/syntax/py.typed -------------------------------------------------------------------------------- /fluent.syntax/fluent/syntax/visitor.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from .ast import BaseNode, Node 4 | 5 | 6 | class Visitor: 7 | """Read-only visitor pattern. 8 | 9 | Subclass this to gather information from an AST. 10 | To generally define which nodes not to descend in to, overload 11 | `generic_visit`. 12 | To handle specific node types, add methods like `visit_Pattern`. 13 | If you want to still descend into the children of the node, call 14 | `generic_visit` of the superclass. 15 | """ 16 | 17 | def visit(self, node: Any) -> None: 18 | if isinstance(node, list): 19 | for child in node: 20 | self.visit(child) 21 | return 22 | if not isinstance(node, BaseNode): 23 | return 24 | nodename = type(node).__name__ 25 | visit = getattr(self, f"visit_{nodename}", self.generic_visit) 26 | visit(node) 27 | 28 | def generic_visit(self, node: BaseNode) -> None: 29 | for propvalue in vars(node).values(): 30 | self.visit(propvalue) 31 | 32 | 33 | class Transformer(Visitor): 34 | """In-place AST Transformer pattern. 35 | 36 | Subclass this to create an in-place modified variant 37 | of the given AST. 38 | If you need to keep the original AST around, pass 39 | a `node.clone()` to the transformer. 40 | """ 41 | 42 | def visit(self, node: Any) -> Any: 43 | if not isinstance(node, BaseNode): 44 | return node 45 | 46 | nodename = type(node).__name__ 47 | visit = getattr(self, f"visit_{nodename}", self.generic_visit) 48 | return visit(node) 49 | 50 | def generic_visit(self, node: Node) -> Node: # type: ignore 51 | for propname, propvalue in vars(node).items(): 52 | if isinstance(propvalue, list): 53 | new_vals: List[Any] = [] 54 | for child in propvalue: 55 | new_val = self.visit(child) 56 | if new_val is not None: 57 | new_vals.append(new_val) 58 | # in-place manipulation 59 | propvalue[:] = new_vals 60 | elif isinstance(propvalue, BaseNode): 61 | new_val = self.visit(propvalue) 62 | if new_val is None: 63 | delattr(node, propname) 64 | else: 65 | setattr(node, propname, new_val) 66 | return node 67 | -------------------------------------------------------------------------------- /fluent.syntax/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version=0.19.0 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /fluent.syntax/setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import setup 4 | 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, "README.rst"), "rb") as f: 7 | long_description = f.read().decode("utf-8") 8 | 9 | setup( 10 | name="fluent.syntax", 11 | description="Localization library for expressive translations.", 12 | long_description=long_description, 13 | long_description_content_type="text/x-rst", 14 | author="Mozilla", 15 | author_email="l10n-drivers@mozilla.org", 16 | license="APL 2", 17 | url="https://github.com/projectfluent/python-fluent", 18 | keywords=["fluent", "localization", "l10n"], 19 | classifiers=[ 20 | "Development Status :: 3 - Alpha", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3 :: Only", 28 | ], 29 | packages=["fluent.syntax"], 30 | package_data={"fluent.syntax": ["py.typed"]}, 31 | install_requires=["typing-extensions>=3.7,<5"], 32 | test_suite="tests.syntax", 33 | ) 34 | -------------------------------------------------------------------------------- /fluent.syntax/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.syntax/tests/__init__.py -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/README.md: -------------------------------------------------------------------------------- 1 | The files in `fixtures_*` are copied from `fluent.js/fluent-syntax`. Due to 2 | the backwards compatibility with Syntax 0.4, the Python parser sometimes 3 | produces different output, mainly in terms of reported errors. Currently, the 4 | files which are known to differ are: 5 | 6 | fixtures_behavior/standalone_identifier.ftl 7 | fixtures_structure/multiline_pattern.ftl 8 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/__init__.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | 4 | def dedent_ftl(text): 5 | return textwrap.dedent(f"{text.rstrip()}\n") 6 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/any_char.ftl: -------------------------------------------------------------------------------- 1 | # ↓ BEL, U+0007 2 | control0 = abcdef 3 | 4 | # ↓ DEL, U+007F 5 | delete = abcdef 6 | 7 | # ↓ BPM, U+0082 8 | control1 = abc‚def 9 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/any_char.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "control0" 9 | }, 10 | "value": { 11 | "type": "Pattern", 12 | "elements": [ 13 | { 14 | "type": "TextElement", 15 | "value": "abc\u0007def" 16 | } 17 | ] 18 | }, 19 | "attributes": [], 20 | "comment": { 21 | "type": "Comment", 22 | "content": " ↓ BEL, U+0007" 23 | } 24 | }, 25 | { 26 | "type": "Message", 27 | "id": { 28 | "type": "Identifier", 29 | "name": "delete" 30 | }, 31 | "value": { 32 | "type": "Pattern", 33 | "elements": [ 34 | { 35 | "type": "TextElement", 36 | "value": "abcdef" 37 | } 38 | ] 39 | }, 40 | "attributes": [], 41 | "comment": { 42 | "type": "Comment", 43 | "content": " ↓ DEL, U+007F" 44 | } 45 | }, 46 | { 47 | "type": "Message", 48 | "id": { 49 | "type": "Identifier", 50 | "name": "control1" 51 | }, 52 | "value": { 53 | "type": "Pattern", 54 | "elements": [ 55 | { 56 | "type": "TextElement", 57 | "value": "abc‚def" 58 | } 59 | ] 60 | }, 61 | "attributes": [], 62 | "comment": { 63 | "type": "Comment", 64 | "content": " ↓ BPM, U+0082" 65 | } 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/astral.ftl: -------------------------------------------------------------------------------- 1 | face-with-tears-of-joy = 😂 2 | tetragram-for-centre = 𝌆 3 | 4 | surrogates-in-text = \uD83D\uDE02 5 | surrogates-in-string = {"\uD83D\uDE02"} 6 | surrogates-in-adjacent-strings = {"\uD83D"}{"\uDE02"} 7 | 8 | emoji-in-text = A face 😂 with tears of joy. 9 | emoji-in-string = {"A face 😂 with tears of joy."} 10 | 11 | # ERROR Invalid identifier 12 | err-😂 = Value 13 | 14 | # ERROR Invalid expression 15 | err-invalid-expression = { 😂 } 16 | 17 | # ERROR Invalid variant key 18 | err-invalid-variant-key = { $sel -> 19 | *[😂] Value 20 | } 21 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl: -------------------------------------------------------------------------------- 1 | ## Function names 2 | 3 | valid-func-name-01 = {FUN1()} 4 | valid-func-name-02 = {FUN_FUN()} 5 | valid-func-name-03 = {FUN-FUN()} 6 | 7 | # JUNK 0 is not a valid Identifier start 8 | invalid-func-name-01 = {0FUN()} 9 | # JUNK Function names may not be lowercase 10 | invalid-func-name-02 = {fun()} 11 | # JUNK Function names may not contain lowercase character 12 | invalid-func-name-03 = {Fun()} 13 | # JUNK ? is not a valid Identifier character 14 | invalid-func-name-04 = {FUN?()} 15 | 16 | ## Arguments 17 | 18 | positional-args = {FUN(1, "a", msg)} 19 | named-args = {FUN(x: 1, y: "Y")} 20 | dense-named-args = {FUN(x:1, y:"Y")} 21 | mixed-args = {FUN(1, "a", msg, x: 1, y: "Y")} 22 | 23 | # ERROR Positional arg must not follow keyword args 24 | shuffled-args = {FUN(1, x: 1, "a", y: "Y", msg)} 25 | 26 | # ERROR Named arguments must be unique 27 | duplicate-named-args = {FUN(x: 1, x: "X")} 28 | 29 | 30 | ## Whitespace around arguments 31 | 32 | sparse-inline-call = {FUN ( "a" , msg, x: 1 )} 33 | empty-inline-call = {FUN( )} 34 | multiline-call = {FUN( 35 | "a", 36 | msg, 37 | x: 1 38 | )} 39 | sparse-multiline-call = {FUN 40 | ( 41 | 42 | "a" , 43 | msg 44 | , x: 1 45 | )} 46 | empty-multiline-call = {FUN( 47 | 48 | )} 49 | 50 | 51 | unindented-arg-number = {FUN( 52 | 1)} 53 | 54 | unindented-arg-string = {FUN( 55 | "a")} 56 | 57 | unindented-arg-msg-ref = {FUN( 58 | msg)} 59 | 60 | unindented-arg-term-ref = {FUN( 61 | -msg)} 62 | 63 | unindented-arg-var-ref = {FUN( 64 | $var)} 65 | 66 | unindented-arg-call = {FUN( 67 | OTHER())} 68 | 69 | unindented-named-arg = {FUN( 70 | x:1)} 71 | 72 | unindented-closing-paren = {FUN( 73 | x 74 | )} 75 | 76 | 77 | 78 | ## Optional trailing comma 79 | 80 | one-argument = {FUN(1,)} 81 | many-arguments = {FUN(1, 2, 3,)} 82 | inline-sparse-args = {FUN( 1, 2, 3, )} 83 | mulitline-args = {FUN( 84 | 1, 85 | 2, 86 | )} 87 | mulitline-sparse-args = {FUN( 88 | 89 | 1 90 | , 91 | 2 92 | , 93 | )} 94 | 95 | 96 | ## Syntax errors for trailing comma 97 | 98 | one-argument = {FUN(1,,)} 99 | missing-arg = {FUN(,)} 100 | missing-sparse-arg = {FUN( , )} 101 | 102 | 103 | ## Whitespace in named arguments 104 | 105 | sparse-named-arg = {FUN( 106 | x : 1, 107 | y : 2, 108 | z 109 | : 110 | 3 111 | )} 112 | 113 | 114 | unindented-colon = {FUN( 115 | x 116 | :1)} 117 | 118 | unindented-value = {FUN( 119 | x: 120 | 1)} 121 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/callee_expressions.ftl: -------------------------------------------------------------------------------- 1 | ## Callees in placeables. 2 | 3 | function-callee-placeable = {FUNCTION()} 4 | term-callee-placeable = {-term()} 5 | 6 | # ERROR Messages cannot be parameterized. 7 | message-callee-placeable = {message()} 8 | # ERROR Equivalent to a MessageReference callee. 9 | mixed-case-callee-placeable = {Function()} 10 | # ERROR Message attributes cannot be parameterized. 11 | message-attr-callee-placeable = {message.attr()} 12 | # ERROR Term attributes may not be used in Placeables. 13 | term-attr-callee-placeable = {-term.attr()} 14 | # ERROR Variables cannot be parameterized. 15 | variable-callee-placeable = {$variable()} 16 | 17 | 18 | ## Callees in selectors. 19 | 20 | function-callee-selector = {FUNCTION() -> 21 | *[key] Value 22 | } 23 | term-attr-callee-selector = {-term.attr() -> 24 | *[key] Value 25 | } 26 | 27 | # ERROR Messages cannot be parameterized. 28 | message-callee-selector = {message() -> 29 | *[key] Value 30 | } 31 | # ERROR Equivalent to a MessageReference callee. 32 | mixed-case-callee-selector = {Function() -> 33 | *[key] Value 34 | } 35 | # ERROR Message attributes cannot be parameterized. 36 | message-attr-callee-selector = {message.attr() -> 37 | *[key] Value 38 | } 39 | # ERROR Term values may not be used as selectors. 40 | term-callee-selector = {-term() -> 41 | *[key] Value 42 | } 43 | # ERROR Variables cannot be parameterized. 44 | variable-callee-selector = {$variable() -> 45 | *[key] Value 46 | } 47 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/comments.ftl: -------------------------------------------------------------------------------- 1 | # Standalone Comment 2 | 3 | # Message Comment 4 | foo = Foo 5 | 6 | # Term Comment 7 | # with a blank last line. 8 | # 9 | -term = Term 10 | 11 | # Another standalone 12 | # 13 | # with indent 14 | ## Group Comment 15 | ### Resource Comment 16 | 17 | # Errors 18 | #error 19 | ##error 20 | ###error 21 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Comment", 6 | "content": "Standalone Comment" 7 | }, 8 | { 9 | "type": "Message", 10 | "id": { 11 | "type": "Identifier", 12 | "name": "foo" 13 | }, 14 | "value": { 15 | "type": "Pattern", 16 | "elements": [ 17 | { 18 | "type": "TextElement", 19 | "value": "Foo" 20 | } 21 | ] 22 | }, 23 | "attributes": [], 24 | "comment": { 25 | "type": "Comment", 26 | "content": "Message Comment" 27 | } 28 | }, 29 | { 30 | "type": "Term", 31 | "id": { 32 | "type": "Identifier", 33 | "name": "term" 34 | }, 35 | "value": { 36 | "type": "Pattern", 37 | "elements": [ 38 | { 39 | "type": "TextElement", 40 | "value": "Term" 41 | } 42 | ] 43 | }, 44 | "attributes": [], 45 | "comment": { 46 | "type": "Comment", 47 | "content": "Term Comment\nwith a blank last line.\n" 48 | } 49 | }, 50 | { 51 | "type": "Comment", 52 | "content": "Another standalone\n\n with indent" 53 | }, 54 | { 55 | "type": "GroupComment", 56 | "content": "Group Comment" 57 | }, 58 | { 59 | "type": "ResourceComment", 60 | "content": "Resource Comment" 61 | }, 62 | { 63 | "type": "Comment", 64 | "content": "Errors" 65 | }, 66 | { 67 | "type": "Junk", 68 | "annotations": [], 69 | "content": "#error\n" 70 | }, 71 | { 72 | "type": "Junk", 73 | "annotations": [], 74 | "content": "##error\n" 75 | }, 76 | { 77 | "type": "Junk", 78 | "annotations": [], 79 | "content": "###error\n" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/cr.ftl: -------------------------------------------------------------------------------- 1 | ### This entire file uses CR as EOL. err01 = Value 01 err02 = Value 02 err03 = Value 03 Continued .title = Title err04 = { "str err05 = { $sel -> } -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/cr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "This entire file uses CR as EOL.\r\rerr01 = Value 01\rerr02 = Value 02\r\rerr03 =\r\r Value 03\r Continued\r\r .title = Title\r\rerr04 = { \"str\r\rerr05 = { $sel -> }\r" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/crlf.ftl: -------------------------------------------------------------------------------- 1 | 2 | key01 = Value 01 3 | key02 = 4 | 5 | Value 02 6 | Continued 7 | 8 | .title = Title 9 | 10 | # ERROR Unclosed StringLiteral 11 | err03 = { "str 12 | 13 | # ERROR Missing newline after ->. 14 | err04 = { $sel -> } 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/crlf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key01" 9 | }, 10 | "value": { 11 | "type": "Pattern", 12 | "elements": [ 13 | { 14 | "type": "TextElement", 15 | "value": "Value 01" 16 | } 17 | ] 18 | }, 19 | "attributes": [], 20 | "comment": null 21 | }, 22 | { 23 | "type": "Message", 24 | "id": { 25 | "type": "Identifier", 26 | "name": "key02" 27 | }, 28 | "value": { 29 | "type": "Pattern", 30 | "elements": [ 31 | { 32 | "type": "TextElement", 33 | "value": "Value 02\nContinued" 34 | } 35 | ] 36 | }, 37 | "attributes": [ 38 | { 39 | "type": "Attribute", 40 | "id": { 41 | "type": "Identifier", 42 | "name": "title" 43 | }, 44 | "value": { 45 | "type": "Pattern", 46 | "elements": [ 47 | { 48 | "type": "TextElement", 49 | "value": "Title" 50 | } 51 | ] 52 | } 53 | } 54 | ], 55 | "comment": null 56 | }, 57 | { 58 | "type": "Comment", 59 | "content": "ERROR Unclosed StringLiteral" 60 | }, 61 | { 62 | "type": "Junk", 63 | "annotations": [], 64 | "content": "err03 = { \"str\r\n\r\n" 65 | }, 66 | { 67 | "type": "Comment", 68 | "content": "ERROR Missing newline after ->." 69 | }, 70 | { 71 | "type": "Junk", 72 | "annotations": [], 73 | "content": "err04 = { $sel -> }\r\n" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_comment.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion when editing this file. 2 | 3 | # No EOL -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "NOTE: Disable final newline insertion when editing this file." 7 | }, 8 | { 9 | "type": "Comment", 10 | "content": "No EOL" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_empty.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.syntax/tests/syntax/fixtures_reference/eof_empty.ftl -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [] 4 | } 5 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_id.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion when editing this file. 2 | 3 | message-id -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "NOTE: Disable final newline insertion when editing this file." 7 | }, 8 | { 9 | "type": "Junk", 10 | "annotations": [], 11 | "content": "message-id" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion when editing this file. 2 | 3 | message-id = -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_id_equals.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "NOTE: Disable final newline insertion when editing this file." 7 | }, 8 | { 9 | "type": "Junk", 10 | "annotations": [], 11 | "content": "message-id =" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_junk.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion when editing this file. 2 | 3 | 000 -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_junk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "NOTE: Disable final newline insertion when editing this file." 7 | }, 8 | { 9 | "type": "Junk", 10 | "annotations": [], 11 | "content": "000" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_value.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion when editing this file. 2 | 3 | no-eol = No EOL -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/eof_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "NOTE: Disable final newline insertion when editing this file." 7 | }, 8 | { 9 | "type": "Message", 10 | "id": { 11 | "type": "Identifier", 12 | "name": "no-eol" 13 | }, 14 | "value": { 15 | "type": "Pattern", 16 | "elements": [ 17 | { 18 | "type": "TextElement", 19 | "value": "No EOL" 20 | } 21 | ] 22 | }, 23 | "attributes": [], 24 | "comment": null 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/escaped_characters.ftl: -------------------------------------------------------------------------------- 1 | ## Literal text 2 | text-backslash-one = Value with \ a backslash 3 | text-backslash-two = Value with \\ two backslashes 4 | text-backslash-brace = Value with \{placeable} 5 | text-backslash-u = \u0041 6 | text-backslash-backslash-u = \\u0041 7 | 8 | ## String literals 9 | quote-in-string = {"\""} 10 | backslash-in-string = {"\\"} 11 | # ERROR Mismatched quote 12 | mismatched-quote = {"\\""} 13 | # ERROR Unknown escape 14 | unknown-escape = {"\x"} 15 | # ERROR Multiline literal 16 | invalid-multiline-literal = {" 17 | "} 18 | 19 | ## Unicode escapes 20 | string-unicode-4digits = {"\u0041"} 21 | escape-unicode-4digits = {"\\u0041"} 22 | string-unicode-6digits = {"\U01F602"} 23 | escape-unicode-6digits = {"\\U01F602"} 24 | 25 | # OK The trailing "00" is part of the literal value. 26 | string-too-many-4digits = {"\u004100"} 27 | # OK The trailing "00" is part of the literal value. 28 | string-too-many-6digits = {"\U01F60200"} 29 | 30 | # ERROR Too few hex digits after \u. 31 | string-too-few-4digits = {"\u41"} 32 | # ERROR Too few hex digits after \U. 33 | string-too-few-6digits = {"\U1F602"} 34 | 35 | ## Literal braces 36 | brace-open = An opening {"{"} brace. 37 | brace-close = A closing {"}"} brace. 38 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/junk.ftl: -------------------------------------------------------------------------------- 1 | ## Two adjacent Junks. 2 | err01 = {1x} 3 | err02 = {2x} 4 | 5 | # A single Junk. 6 | err03 = {1x 7 | 2 8 | 9 | # A single Junk. 10 | ą=Invalid identifier 11 | ć=Another one 12 | 13 | # The COMMENT ends this junk. 14 | err04 = { 15 | # COMMENT 16 | 17 | # The COMMENT ends this junk. 18 | # The closing brace is a separate Junk. 19 | err04 = { 20 | # COMMENT 21 | } 22 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/junk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "GroupComment", 6 | "content": "Two adjacent Junks." 7 | }, 8 | { 9 | "type": "Junk", 10 | "annotations": [], 11 | "content": "err01 = {1x}\n" 12 | }, 13 | { 14 | "type": "Junk", 15 | "annotations": [], 16 | "content": "err02 = {2x}\n\n" 17 | }, 18 | { 19 | "type": "Comment", 20 | "content": "A single Junk." 21 | }, 22 | { 23 | "type": "Junk", 24 | "annotations": [], 25 | "content": "err03 = {1x\n2\n\n" 26 | }, 27 | { 28 | "type": "Comment", 29 | "content": "A single Junk." 30 | }, 31 | { 32 | "type": "Junk", 33 | "annotations": [], 34 | "content": "ą=Invalid identifier\nć=Another one\n\n" 35 | }, 36 | { 37 | "type": "Comment", 38 | "content": "The COMMENT ends this junk." 39 | }, 40 | { 41 | "type": "Junk", 42 | "annotations": [], 43 | "content": "err04 = {\n" 44 | }, 45 | { 46 | "type": "Comment", 47 | "content": "COMMENT" 48 | }, 49 | { 50 | "type": "Comment", 51 | "content": "The COMMENT ends this junk.\nThe closing brace is a separate Junk." 52 | }, 53 | { 54 | "type": "Junk", 55 | "annotations": [], 56 | "content": "err04 = {\n" 57 | }, 58 | { 59 | "type": "Comment", 60 | "content": "COMMENT" 61 | }, 62 | { 63 | "type": "Junk", 64 | "annotations": [], 65 | "content": "}\n" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/leading_dots.ftl: -------------------------------------------------------------------------------- 1 | key01 = .Value 2 | key02 = …Value 3 | key03 = {"."}Value 4 | key04 = 5 | {"."}Value 6 | 7 | key05 = Value 8 | {"."}Continued 9 | 10 | key06 = .Value 11 | {"."}Continued 12 | 13 | # MESSAGE (value = "Value", attributes = []) 14 | # JUNK (attr .Continued" must have a value) 15 | key07 = Value 16 | .Continued 17 | 18 | # JUNK (attr .Value must have a value) 19 | key08 = 20 | .Value 21 | 22 | # JUNK (attr .Value must have a value) 23 | key09 = 24 | .Value 25 | Continued 26 | 27 | key10 = 28 | .Value = which is an attribute 29 | Continued 30 | 31 | key11 = 32 | {"."}Value = which looks like an attribute 33 | Continued 34 | 35 | key12 = 36 | .accesskey = 37 | A 38 | 39 | key13 = 40 | .attribute = .Value 41 | 42 | key14 = 43 | .attribute = 44 | {"."}Value 45 | 46 | key15 = 47 | { 1 -> 48 | [one] .Value 49 | *[other] 50 | {"."}Value 51 | } 52 | 53 | # JUNK (variant must have a value) 54 | key16 = 55 | { 1 -> 56 | *[one] 57 | .Value 58 | } 59 | 60 | # JUNK (unclosed placeable) 61 | key17 = 62 | { 1 -> 63 | *[one] Value 64 | .Continued 65 | } 66 | 67 | # JUNK (attr .Value must have a value) 68 | key18 = 69 | .Value 70 | 71 | key19 = 72 | .attribute = Value 73 | Continued 74 | 75 | key20 = 76 | {"."}Value 77 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.ftl: -------------------------------------------------------------------------------- 1 | string-expression = {"abc"} 2 | number-expression = {123} 3 | number-expression = {-3.14} 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/literal_expressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "string-expression" 9 | }, 10 | "value": { 11 | "type": "Pattern", 12 | "elements": [ 13 | { 14 | "type": "Placeable", 15 | "expression": { 16 | "value": "abc", 17 | "type": "StringLiteral" 18 | } 19 | } 20 | ] 21 | }, 22 | "attributes": [], 23 | "comment": null 24 | }, 25 | { 26 | "type": "Message", 27 | "id": { 28 | "type": "Identifier", 29 | "name": "number-expression" 30 | }, 31 | "value": { 32 | "type": "Pattern", 33 | "elements": [ 34 | { 35 | "type": "Placeable", 36 | "expression": { 37 | "value": "123", 38 | "type": "NumberLiteral" 39 | } 40 | } 41 | ] 42 | }, 43 | "attributes": [], 44 | "comment": null 45 | }, 46 | { 47 | "type": "Message", 48 | "id": { 49 | "type": "Identifier", 50 | "name": "number-expression" 51 | }, 52 | "value": { 53 | "type": "Pattern", 54 | "elements": [ 55 | { 56 | "type": "Placeable", 57 | "expression": { 58 | "value": "-3.14", 59 | "type": "NumberLiteral" 60 | } 61 | } 62 | ] 63 | }, 64 | "attributes": [], 65 | "comment": null 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/member_expressions.ftl: -------------------------------------------------------------------------------- 1 | ## Member expressions in placeables. 2 | 3 | # OK Message attributes may be interpolated in values. 4 | message-attribute-expression-placeable = {msg.attr} 5 | 6 | # ERROR Term attributes may not be used for interpolation. 7 | term-attribute-expression-placeable = {-term.attr} 8 | 9 | 10 | ## Member expressions in selectors. 11 | 12 | # OK Term attributes may be used as selectors. 13 | term-attribute-expression-selector = {-term.attr -> 14 | *[key] Value 15 | } 16 | # ERROR Message attributes may not be used as selectors. 17 | message-attribute-expression-selector = {msg.attr -> 18 | *[key] Value 19 | } 20 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/messages.ftl: -------------------------------------------------------------------------------- 1 | key01 = Value 2 | 3 | key02 = Value 4 | .attr = Attribute 5 | 6 | key02 = Value 7 | .attr1 = Attribute 1 8 | .attr2 = Attribute 2 9 | 10 | key03 = 11 | .attr = Attribute 12 | 13 | key04 = 14 | .attr1 = Attribute 1 15 | .attr2 = Attribute 2 16 | 17 | # < whitespace > 18 | key05 = 19 | .attr1 = Attribute 1 20 | 21 | no-whitespace=Value 22 | .attr1=Attribute 1 23 | 24 | extra-whitespace = Value 25 | .attr1 = Attribute 1 26 | 27 | key06 = {""} 28 | 29 | # JUNK Missing value 30 | key07 = 31 | 32 | # JUNK Missing = 33 | key08 34 | 35 | KEY09 = Value 09 36 | 37 | key-10 = Value 10 38 | key_11 = Value 11 39 | key-12- = Value 12 40 | key_13_ = Value 13 41 | 42 | # JUNK Invalid id 43 | 0err-14 = Value 14 44 | 45 | # JUNK Invalid id 46 | err-15? = Value 15 47 | 48 | # JUNK Invalid id 49 | err-ąę-16 = Value 16 50 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/mixed_entries.ftl: -------------------------------------------------------------------------------- 1 | # License Comment 2 | 3 | ### Resource Comment 4 | 5 | -brand-name = Aurora 6 | 7 | ## Group Comment 8 | 9 | key01 = 10 | .attr = Attribute 11 | 12 | ą=Invalid identifier 13 | ć=Another one 14 | 15 | # Message Comment 16 | key02 = Value 17 | 18 | # Standalone Comment 19 | .attr = Dangling attribute 20 | 21 | # There are 5 spaces on the line between key03 and key04. 22 | key03 = Value 03 23 | 24 | key04 = Value 04 25 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/multiline_values.ftl: -------------------------------------------------------------------------------- 1 | key01 = A multiline value 2 | continued on the next line 3 | 4 | and also down here. 5 | 6 | key02 = 7 | A multiline value starting 8 | on a new line. 9 | 10 | key03 = 11 | .attr = A multiline attribute value 12 | continued on the next line 13 | 14 | and also down here. 15 | 16 | key04 = 17 | .attr = 18 | A multiline attribute value 19 | staring on a new line 20 | 21 | key05 = 22 | 23 | A multiline value with non-standard 24 | 25 | indentation. 26 | 27 | key06 = 28 | A multiline value with {"placeables"} 29 | {"at"} the beginning and the end 30 | {"of lines"}{"."} 31 | 32 | key07 = 33 | {"A multiline value"} starting and ending {"with a placeable"} 34 | 35 | key08 = Leading and trailing whitespace. 36 | 37 | key09 = zero 38 | three 39 | two 40 | one 41 | zero 42 | 43 | key10 = 44 | two 45 | zero 46 | four 47 | 48 | key11 = 49 | 50 | 51 | two 52 | zero 53 | 54 | key12 = 55 | {"."} 56 | four 57 | 58 | key13 = 59 | four 60 | {"."} 61 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/numbers.ftl: -------------------------------------------------------------------------------- 1 | int-zero = {0} 2 | int-positive = {1} 3 | int-negative = {-1} 4 | int-negative-zero = {-0} 5 | 6 | int-positive-padded = {01} 7 | int-negative-padded = {-01} 8 | int-zero-padded = {00} 9 | int-negative-zero-padded = {-00} 10 | 11 | float-zero = {0.0} 12 | float-positive = {0.01} 13 | float-positive-one = {1.03} 14 | float-positive-without-fraction = {1.000} 15 | 16 | float-negative = {-0.01} 17 | float-negative-one = {-1.03} 18 | float-negative-zero = {-0.0} 19 | float-negative-without-fraction = {-1.000} 20 | 21 | float-positive-padded-left = {01.03} 22 | float-positive-padded-right = {1.0300} 23 | float-positive-padded-both = {01.0300} 24 | 25 | float-negative-padded-left = {-01.03} 26 | float-negative-padded-right = {-1.0300} 27 | float-negative-padded-both = {-01.0300} 28 | 29 | 30 | ## ERRORS 31 | 32 | err01 = {1.} 33 | err02 = {.02} 34 | err03 = {1.02.03} 35 | err04 = {1. 02} 36 | err05 = {1 .02} 37 | err06 = {- 1} 38 | err07 = {1,02} 39 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/obsolete.ftl: -------------------------------------------------------------------------------- 1 | ### The syntax in this file has been discontinued. It is no longer part of the 2 | ### Fluent specification and should not be implemented nor used. We're keeping 3 | ### these fixtures around to protect against accidental syntax reuse. 4 | 5 | 6 | ## Variant lists. 7 | 8 | message-variant-list = 9 | { 10 | *[key] Value 11 | } 12 | 13 | -term-variant-list = 14 | { 15 | *[key] Value 16 | } 17 | 18 | 19 | ## Variant expressions. 20 | 21 | message-variant-expression-placeable = {msg[case]} 22 | message-variant-expression-selector = {msg[case] -> 23 | *[key] Value 24 | } 25 | 26 | term-variant-expression-placeable = {-term[case]} 27 | term-variant-expression-selector = {-term[case] -> 28 | *[key] Value 29 | } 30 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/obsolete.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "The syntax in this file has been discontinued. It is no longer part of the\nFluent specification and should not be implemented nor used. We're keeping\nthese fixtures around to protect against accidental syntax reuse." 7 | }, 8 | { 9 | "type": "GroupComment", 10 | "content": "Variant lists." 11 | }, 12 | { 13 | "type": "Junk", 14 | "annotations": [], 15 | "content": "message-variant-list =\n {\n *[key] Value\n }\n\n" 16 | }, 17 | { 18 | "type": "Junk", 19 | "annotations": [], 20 | "content": "-term-variant-list =\n {\n *[key] Value\n }\n\n\n" 21 | }, 22 | { 23 | "type": "GroupComment", 24 | "content": "Variant expressions." 25 | }, 26 | { 27 | "type": "Junk", 28 | "annotations": [], 29 | "content": "message-variant-expression-placeable = {msg[case]}\n" 30 | }, 31 | { 32 | "type": "Junk", 33 | "annotations": [], 34 | "content": "message-variant-expression-selector = {msg[case] ->\n *[key] Value\n}\n\n" 35 | }, 36 | { 37 | "type": "Junk", 38 | "annotations": [], 39 | "content": "term-variant-expression-placeable = {-term[case]}\n" 40 | }, 41 | { 42 | "type": "Junk", 43 | "annotations": [], 44 | "content": "term-variant-expression-selector = {-term[case] ->\n *[key] Value\n}\n" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/placeables.ftl: -------------------------------------------------------------------------------- 1 | nested-placeable = {{{1}}} 2 | padded-placeable = { 1 } 3 | sparse-placeable = { { 1 } } 4 | 5 | # ERROR Unmatched opening brace 6 | unmatched-open1 = { 1 7 | 8 | # ERROR Unmatched opening brace 9 | unmatched-open2 = {{ 1 } 10 | 11 | # ERROR Unmatched closing brace 12 | unmatched-close1 = 1 } 13 | 14 | # ERROR Unmatched closing brace 15 | unmatched-close2 = { 1 }} 16 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/reference_expressions.ftl: -------------------------------------------------------------------------------- 1 | ## Reference expressions in placeables. 2 | 3 | message-reference-placeable = {msg} 4 | term-reference-placeable = {-term} 5 | variable-reference-placeable = {$var} 6 | 7 | # Function references are invalid outside of call expressions. 8 | # This parses as a valid MessageReference. 9 | function-reference-placeable = {FUN} 10 | 11 | 12 | ## Reference expressions in selectors. 13 | 14 | variable-reference-selector = {$var -> 15 | *[key] Value 16 | } 17 | 18 | # ERROR Message values may not be used as selectors. 19 | message-reference-selector = {msg -> 20 | *[key] Value 21 | } 22 | # ERROR Term values may not be used as selectors. 23 | term-reference-selector = {-term -> 24 | *[key] Value 25 | } 26 | # ERROR Function references are invalid outside of call expressions, and this 27 | # parses as a MessageReference which isn't a valid selector. 28 | function-expression-selector = {FUN -> 29 | *[key] Value 30 | } 31 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/select_expressions.ftl: -------------------------------------------------------------------------------- 1 | new-messages = 2 | { BUILTIN() -> 3 | [0] Zero 4 | *[other] {""}Other 5 | } 6 | 7 | valid-selector-term-attribute = 8 | { -term.case -> 9 | *[key] value 10 | } 11 | 12 | # ERROR Term values are not valid selectors 13 | invalid-selector-term-value = 14 | { -term -> 15 | *[key] value 16 | } 17 | 18 | # ERROR CallExpressions on Terms are similar to TermReferences 19 | invalid-selector-term-variant = 20 | { -term(case: "nominative") -> 21 | *[key] value 22 | } 23 | 24 | # ERROR Nested expressions are not valid selectors 25 | invalid-selector-nested-expression = 26 | { { 3 } -> 27 | *[key] default 28 | } 29 | 30 | # ERROR Select expressions are not valid selectors 31 | invalid-selector-select-expression = 32 | { { $sel -> 33 | *[key] value 34 | } -> 35 | *[key] default 36 | } 37 | 38 | empty-variant = 39 | { $sel -> 40 | *[key] {""} 41 | } 42 | 43 | reduced-whitespace = 44 | {FOO()-> 45 | *[key] {""} 46 | } 47 | 48 | nested-select = 49 | { $sel -> 50 | *[one] { $sel -> 51 | *[two] Value 52 | } 53 | } 54 | 55 | # ERROR Missing selector 56 | missing-selector = 57 | { 58 | *[key] Value 59 | } 60 | 61 | # ERROR Missing line end after variant list 62 | missing-line-end = 63 | { $sel -> 64 | *[key] Value} 65 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/select_indent.ftl: -------------------------------------------------------------------------------- 1 | select-1tbs-inline = { $selector -> 2 | *[key] Value 3 | } 4 | 5 | select-1tbs-newline = { 6 | $selector -> 7 | *[key] Value 8 | } 9 | 10 | select-1tbs-indent = { 11 | $selector -> 12 | *[key] Value 13 | } 14 | 15 | select-allman-inline = 16 | { $selector -> 17 | *[key] Value 18 | [other] Other 19 | } 20 | 21 | select-allman-newline = 22 | { 23 | $selector -> 24 | *[key] Value 25 | } 26 | 27 | select-allman-indent = 28 | { 29 | $selector -> 30 | *[key] Value 31 | } 32 | 33 | select-gnu-inline = 34 | { $selector -> 35 | *[key] Value 36 | } 37 | 38 | select-gnu-newline = 39 | { 40 | $selector -> 41 | *[key] Value 42 | } 43 | 44 | select-gnu-indent = 45 | { 46 | $selector -> 47 | *[key] Value 48 | } 49 | 50 | select-no-indent = 51 | { 52 | $selector -> 53 | *[key] Value 54 | [other] Other 55 | } 56 | 57 | select-no-indent-multiline = 58 | { 59 | $selector -> 60 | *[key] Value 61 | Continued 62 | [other] 63 | Other 64 | Multiline 65 | } 66 | 67 | # ERROR (Multiline text must be indented) 68 | select-no-indent-multiline = { $selector -> 69 | *[key] Value 70 | Continued without indent. 71 | } 72 | 73 | select-flat = 74 | { 75 | $selector 76 | -> 77 | *[ 78 | key 79 | ] Value 80 | [ 81 | other 82 | ] Other 83 | } 84 | 85 | # Each line ends with 5 spaces. 86 | select-flat-with-trailing-spaces = 87 | { 88 | $selector 89 | -> 90 | *[ 91 | key 92 | ] Value 93 | [ 94 | other 95 | ] Other 96 | } 97 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/sparse_entries.ftl: -------------------------------------------------------------------------------- 1 | key01 = 2 | 3 | 4 | Value 5 | 6 | key02 = 7 | 8 | 9 | .attr = Attribute 10 | 11 | 12 | key03 = 13 | Value 14 | Continued 15 | 16 | 17 | Over multiple 18 | Lines 19 | 20 | 21 | 22 | .attr = Attribute 23 | 24 | 25 | key05 = Value 26 | 27 | key06 = { 1 -> 28 | 29 | 30 | [one] One 31 | 32 | 33 | 34 | 35 | *[two] Two 36 | 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/special_chars.ftl: -------------------------------------------------------------------------------- 1 | ## OK 2 | 3 | bracket-inline = [Value] 4 | dot-inline = .Value 5 | star-inline = *Value 6 | 7 | ## ERRORS 8 | 9 | bracket-newline = 10 | [Value] 11 | dot-newline = 12 | .Value 13 | star-newline = 14 | *Value 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/special_chars.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "GroupComment", 6 | "content": "OK" 7 | }, 8 | { 9 | "type": "Message", 10 | "id": { 11 | "type": "Identifier", 12 | "name": "bracket-inline" 13 | }, 14 | "value": { 15 | "type": "Pattern", 16 | "elements": [ 17 | { 18 | "type": "TextElement", 19 | "value": "[Value]" 20 | } 21 | ] 22 | }, 23 | "attributes": [], 24 | "comment": null 25 | }, 26 | { 27 | "type": "Message", 28 | "id": { 29 | "type": "Identifier", 30 | "name": "dot-inline" 31 | }, 32 | "value": { 33 | "type": "Pattern", 34 | "elements": [ 35 | { 36 | "type": "TextElement", 37 | "value": ".Value" 38 | } 39 | ] 40 | }, 41 | "attributes": [], 42 | "comment": null 43 | }, 44 | { 45 | "type": "Message", 46 | "id": { 47 | "type": "Identifier", 48 | "name": "star-inline" 49 | }, 50 | "value": { 51 | "type": "Pattern", 52 | "elements": [ 53 | { 54 | "type": "TextElement", 55 | "value": "*Value" 56 | } 57 | ] 58 | }, 59 | "attributes": [], 60 | "comment": null 61 | }, 62 | { 63 | "type": "GroupComment", 64 | "content": "ERRORS" 65 | }, 66 | { 67 | "type": "Junk", 68 | "annotations": [], 69 | "content": "bracket-newline =\n [Value]\n" 70 | }, 71 | { 72 | "type": "Junk", 73 | "annotations": [], 74 | "content": "dot-newline =\n .Value\n" 75 | }, 76 | { 77 | "type": "Junk", 78 | "annotations": [], 79 | "content": "star-newline =\n *Value\n" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/tab.ftl: -------------------------------------------------------------------------------- 1 | # OK (tab after = is part of the value) 2 | key01 = Value 01 3 | 4 | # Error (tab before =) 5 | key02 = Value 02 6 | 7 | # Error (tab is not a valid indent) 8 | key03 = 9 | This line isn't properly indented. 10 | 11 | # Partial Error (tab is not a valid indent) 12 | key04 = 13 | This line is indented by 4 spaces, 14 | whereas this line by 1 tab. 15 | 16 | # OK (value is a single tab) 17 | key05 = 18 | 19 | # OK (attribute value is two tabs) 20 | key06 = 21 | .attr = 22 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl: -------------------------------------------------------------------------------- 1 | -term = { $arg -> 2 | *[key] Value 3 | } 4 | 5 | key01 = { -term } 6 | key02 = { -term () } 7 | key03 = { -term(arg: 1) } 8 | key04 = { -term("positional", narg1: 1, narg2: 2) } 9 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/terms.ftl: -------------------------------------------------------------------------------- 1 | -term01 = Value 2 | .attr = Attribute 3 | 4 | -term02 = {""} 5 | 6 | # JUNK Missing value 7 | -term03 = 8 | .attr = Attribute 9 | 10 | # JUNK Missing value 11 | # < whitespace > 12 | -term04 = 13 | .attr1 = Attribute 1 14 | 15 | # JUNK Missing value 16 | -term05 = 17 | 18 | # JUNK Missing value 19 | # < whitespace > 20 | -term06 = 21 | 22 | # JUNK Missing = 23 | -term07 24 | 25 | -term08=Value 26 | .attr=Attribute 27 | 28 | -term09 = Value 29 | .attr = Attribute 30 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/variables.ftl: -------------------------------------------------------------------------------- 1 | key01 = {$var} 2 | key02 = { $var } 3 | key03 = { 4 | $var 5 | } 6 | key04 = { 7 | $var} 8 | 9 | 10 | ## Errors 11 | 12 | # ERROR Missing variable identifier 13 | err01 = {$} 14 | # ERROR Double $$ 15 | err02 = {$$var} 16 | # ERROR Invalid first char of the identifier 17 | err03 = {$-var} 18 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/variant_keys.ftl: -------------------------------------------------------------------------------- 1 | simple-identifier = 2 | { $sel -> 3 | *[key] value 4 | } 5 | 6 | identifier-surrounded-by-whitespace = 7 | { $sel -> 8 | *[ key ] value 9 | } 10 | 11 | int-number = 12 | { $sel -> 13 | *[1] value 14 | } 15 | 16 | float-number = 17 | { $sel -> 18 | *[3.14] value 19 | } 20 | 21 | # ERROR 22 | invalid-identifier = 23 | { $sel -> 24 | *[two words] value 25 | } 26 | 27 | # ERROR 28 | invalid-int = 29 | { $sel -> 30 | *[1 apple] value 31 | } 32 | 33 | # ERROR 34 | invalid-int = 35 | { $sel -> 36 | *[3.14 apples] value 37 | } 38 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.ftl: -------------------------------------------------------------------------------- 1 | # Caution, lines 6 and 7 contain white-space-only lines 2 | key = 3 | first line 4 | 5 | 6 | 7 | 8 | 9 | 10 | last line 11 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/whitespace_in_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key" 9 | }, 10 | "value": { 11 | "type": "Pattern", 12 | "elements": [ 13 | { 14 | "type": "TextElement", 15 | "value": "first line\n\n\n\n\n\n\nlast line" 16 | } 17 | ] 18 | }, 19 | "attributes": [], 20 | "comment": { 21 | "type": "Comment", 22 | "content": "Caution, lines 6 and 7 contain white-space-only lines" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/zero_length.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.syntax/tests/syntax/fixtures_reference/zero_length.ftl -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_reference/zero_length.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [] 4 | } 5 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_expression_with_wrong_attr.ftl: -------------------------------------------------------------------------------- 1 | err1 = { foo.23 } 2 | err2 = { foo. } 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_expression_with_wrong_attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0004", 10 | "arguments": [ 11 | "a-zA-Z" 12 | ], 13 | "message": "Expected a character from range: \"a-zA-Z\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 13, 17 | "end": 13 18 | } 19 | } 20 | ], 21 | "content": "err1 = { foo.23 }\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 18 26 | } 27 | }, 28 | { 29 | "type": "Junk", 30 | "annotations": [ 31 | { 32 | "type": "Annotation", 33 | "code": "E0004", 34 | "arguments": [ 35 | "a-zA-Z" 36 | ], 37 | "message": "Expected a character from range: \"a-zA-Z\"", 38 | "span": { 39 | "type": "Span", 40 | "start": 31, 41 | "end": 31 42 | } 43 | } 44 | ], 45 | "content": "err2 = { foo. }\n", 46 | "span": { 47 | "type": "Span", 48 | "start": 18, 49 | "end": 34 50 | } 51 | } 52 | ], 53 | "span": { 54 | "type": "Span", 55 | "start": 0, 56 | "end": 34 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_of_private_as_placeable.ftl: -------------------------------------------------------------------------------- 1 | err1 = { -brand.gender } 2 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_of_private_as_placeable.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0019", 10 | "arguments": [], 11 | "message": "Attributes of terms cannot be used as placeables", 12 | "span": { 13 | "type": "Span", 14 | "start": 23, 15 | "end": 23 16 | } 17 | } 18 | ], 19 | "content": "err1 = { -brand.gender }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 25 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 25 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_of_public_as_selector.ftl: -------------------------------------------------------------------------------- 1 | err1 = 2 | { foo.bar -> 3 | [1] One 4 | *[2] Two 5 | } 6 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_of_public_as_selector.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0018", 10 | "arguments": [], 11 | "message": "Attributes of messages cannot be used as selectors", 12 | "span": { 13 | "type": "Span", 14 | "start": 21, 15 | "end": 21 16 | } 17 | } 18 | ], 19 | "content": "err1 =\n { foo.bar ->\n [1] One\n *[2] Two\n }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 62 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 62 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_starts_from_nl.ftl: -------------------------------------------------------------------------------- 1 | foo = Value 2 | .attr = Value 2 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_starts_from_nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "foo", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 6, 24 | "end": 11 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 6, 31 | "end": 11 32 | } 33 | }, 34 | "attributes": [ 35 | { 36 | "type": "Attribute", 37 | "id": { 38 | "type": "Identifier", 39 | "name": "attr", 40 | "span": { 41 | "type": "Span", 42 | "start": 13, 43 | "end": 17 44 | } 45 | }, 46 | "value": { 47 | "type": "Pattern", 48 | "elements": [ 49 | { 50 | "type": "TextElement", 51 | "value": "Value 2", 52 | "span": { 53 | "type": "Span", 54 | "start": 20, 55 | "end": 27 56 | } 57 | } 58 | ], 59 | "span": { 60 | "type": "Span", 61 | "start": 20, 62 | "end": 27 63 | } 64 | }, 65 | "span": { 66 | "type": "Span", 67 | "start": 12, 68 | "end": 27 69 | } 70 | } 71 | ], 72 | "comment": null, 73 | "span": { 74 | "type": "Span", 75 | "start": 0, 76 | "end": 27 77 | } 78 | } 79 | ], 80 | "span": { 81 | "type": "Span", 82 | "start": 0, 83 | "end": 28 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.ftl: -------------------------------------------------------------------------------- 1 | key1 = Value 1 2 | .attr = 3 | 4 | key2 = 5 | .attr = 6 | 7 | key3 = 8 | .attr1 = Attr 1 9 | .attr2 = 10 | 11 | key4 = 12 | .attr1 = 13 | .attr2 = Attr 2 14 | 15 | key5 = 16 | .attr1 = 17 | .attr2 = 18 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_with_empty_pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0012", 10 | "arguments": [], 11 | "message": "Expected value", 12 | "span": { 13 | "type": "Span", 14 | "start": 26, 15 | "end": 26 16 | } 17 | } 18 | ], 19 | "content": "key1 = Value 1\n .attr =\n\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 28 24 | } 25 | }, 26 | { 27 | "type": "Junk", 28 | "annotations": [ 29 | { 30 | "type": "Annotation", 31 | "code": "E0012", 32 | "arguments": [], 33 | "message": "Expected value", 34 | "span": { 35 | "type": "Span", 36 | "start": 46, 37 | "end": 46 38 | } 39 | } 40 | ], 41 | "content": "key2 =\n .attr =\n\n", 42 | "span": { 43 | "type": "Span", 44 | "start": 28, 45 | "end": 48 46 | } 47 | }, 48 | { 49 | "type": "Junk", 50 | "annotations": [ 51 | { 52 | "type": "Annotation", 53 | "code": "E0012", 54 | "arguments": [], 55 | "message": "Expected value", 56 | "span": { 57 | "type": "Span", 58 | "start": 87, 59 | "end": 87 60 | } 61 | } 62 | ], 63 | "content": "key3 =\n .attr1 = Attr 1\n .attr2 =\n\n", 64 | "span": { 65 | "type": "Span", 66 | "start": 48, 67 | "end": 89 68 | } 69 | }, 70 | { 71 | "type": "Junk", 72 | "annotations": [ 73 | { 74 | "type": "Annotation", 75 | "code": "E0012", 76 | "arguments": [], 77 | "message": "Expected value", 78 | "span": { 79 | "type": "Span", 80 | "start": 108, 81 | "end": 108 82 | } 83 | } 84 | ], 85 | "content": "key4 =\n .attr1 =\n .attr2 = Attr 2\n\n", 86 | "span": { 87 | "type": "Span", 88 | "start": 89, 89 | "end": 130 90 | } 91 | }, 92 | { 93 | "type": "Junk", 94 | "annotations": [ 95 | { 96 | "type": "Annotation", 97 | "code": "E0012", 98 | "arguments": [], 99 | "message": "Expected value", 100 | "span": { 101 | "type": "Span", 102 | "start": 149, 103 | "end": 149 104 | } 105 | } 106 | ], 107 | "content": "key5 =\n .attr1 =\n .attr2 =\n", 108 | "span": { 109 | "type": "Span", 110 | "start": 130, 111 | "end": 163 112 | } 113 | } 114 | ], 115 | "span": { 116 | "type": "Span", 117 | "start": 0, 118 | "end": 163 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_without_equal_sign.ftl: -------------------------------------------------------------------------------- 1 | key = Value 2 | .label 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/attribute_without_equal_sign.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "=" 12 | ], 13 | "message": "Expected token: \"=\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 22, 17 | "end": 22 18 | } 19 | } 20 | ], 21 | "content": "key = Value\n .label\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 23 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 23 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/blank_lines.ftl: -------------------------------------------------------------------------------- 1 | ### NOTE: Disable final newline insertion and trimming when editing this file. 2 | 3 | key01 = Value 01 4 | 5 | key02 = Value 02 6 | 7 | 8 | key03 = 9 | 10 | Value 03 11 | 12 | Continued 13 | 14 | # There are four spaces on the line between "Value 04" and "Continued". 15 | key04 = 16 | 17 | Value 04 18 | 19 | Continued 20 | 21 | # There are four spaces on the line following "Value 05". 22 | key05 = 23 | Value 05 24 | 25 | # There are four spaces on the line following "Value 06". 26 | key06 = Value 06 27 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/broken_number.ftl: -------------------------------------------------------------------------------- 1 | err01 = { -2.4.5 } 2 | err02 = { -2.4. } 3 | err03 = { -.4 } 4 | err04 = { -2..4 } 5 | err05 = { 24d } 6 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/broken_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "}" 12 | ], 13 | "message": "Expected token: \"}\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 14, 17 | "end": 14 18 | } 19 | } 20 | ], 21 | "content": "err01 = { -2.4.5 }\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 19 26 | } 27 | }, 28 | { 29 | "type": "Junk", 30 | "annotations": [ 31 | { 32 | "type": "Annotation", 33 | "code": "E0003", 34 | "arguments": [ 35 | "}" 36 | ], 37 | "message": "Expected token: \"}\"", 38 | "span": { 39 | "type": "Span", 40 | "start": 33, 41 | "end": 33 42 | } 43 | } 44 | ], 45 | "content": "err02 = { -2.4. }\n", 46 | "span": { 47 | "type": "Span", 48 | "start": 19, 49 | "end": 37 50 | } 51 | }, 52 | { 53 | "type": "Junk", 54 | "annotations": [ 55 | { 56 | "type": "Annotation", 57 | "code": "E0004", 58 | "arguments": [ 59 | "a-zA-Z" 60 | ], 61 | "message": "Expected a character from range: \"a-zA-Z\"", 62 | "span": { 63 | "type": "Span", 64 | "start": 48, 65 | "end": 48 66 | } 67 | } 68 | ], 69 | "content": "err03 = { -.4 }\n", 70 | "span": { 71 | "type": "Span", 72 | "start": 37, 73 | "end": 53 74 | } 75 | }, 76 | { 77 | "type": "Junk", 78 | "annotations": [ 79 | { 80 | "type": "Annotation", 81 | "code": "E0004", 82 | "arguments": [ 83 | "0-9" 84 | ], 85 | "message": "Expected a character from range: \"0-9\"", 86 | "span": { 87 | "type": "Span", 88 | "start": 66, 89 | "end": 66 90 | } 91 | } 92 | ], 93 | "content": "err04 = { -2..4 }\n", 94 | "span": { 95 | "type": "Span", 96 | "start": 53, 97 | "end": 71 98 | } 99 | }, 100 | { 101 | "type": "Junk", 102 | "annotations": [ 103 | { 104 | "type": "Annotation", 105 | "code": "E0003", 106 | "arguments": [ 107 | "}" 108 | ], 109 | "message": "Expected token: \"}\"", 110 | "span": { 111 | "type": "Span", 112 | "start": 83, 113 | "end": 83 114 | } 115 | } 116 | ], 117 | "content": "err05 = { 24d }\n", 118 | "span": { 119 | "type": "Span", 120 | "start": 71, 121 | "end": 87 122 | } 123 | } 124 | ], 125 | "span": { 126 | "type": "Span", 127 | "start": 0, 128 | "end": 87 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.ftl: -------------------------------------------------------------------------------- 1 | err01 = { no-caps-name() } 2 | err02 = { BUILTIN(2: "foo") } 3 | err03 = { BUILTIN(key: foo) } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0008", 10 | "arguments": [], 11 | "message": "The callee has to be an upper-case identifier or a term", 12 | "span": { 13 | "type": "Span", 14 | "start": 22, 15 | "end": 22 16 | } 17 | } 18 | ], 19 | "content": "err01 = { no-caps-name() }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 27 24 | } 25 | }, 26 | { 27 | "type": "Junk", 28 | "annotations": [ 29 | { 30 | "type": "Annotation", 31 | "code": "E0009", 32 | "arguments": [], 33 | "message": "The argument name has to be a simple identifier", 34 | "span": { 35 | "type": "Span", 36 | "start": 46, 37 | "end": 46 38 | } 39 | } 40 | ], 41 | "content": "err02 = { BUILTIN(2: \"foo\") }\n", 42 | "span": { 43 | "type": "Span", 44 | "start": 27, 45 | "end": 57 46 | } 47 | }, 48 | { 49 | "type": "Junk", 50 | "annotations": [ 51 | { 52 | "type": "Annotation", 53 | "code": "E0014", 54 | "arguments": [], 55 | "message": "Expected literal", 56 | "span": { 57 | "type": "Span", 58 | "start": 80, 59 | "end": 80 60 | } 61 | } 62 | ], 63 | "content": "err03 = { BUILTIN(key: foo) }\n", 64 | "span": { 65 | "type": "Span", 66 | "start": 57, 67 | "end": 87 68 | } 69 | } 70 | ], 71 | "span": { 72 | "type": "Span", 73 | "start": 0, 74 | "end": 87 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/comment_with_eof.ftl: -------------------------------------------------------------------------------- 1 | # This is a comment with no new line -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/comment_with_eof.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Comment", 6 | "content": "This is a comment with no new line", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 36 11 | } 12 | } 13 | ], 14 | "span": { 15 | "type": "Span", 16 | "start": 0, 17 | "end": 36 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/crlf.ftl: -------------------------------------------------------------------------------- 1 | 2 | key01 = Value 01 3 | key02 = 4 | 5 | Value 02 6 | Continued 7 | 8 | .title = Title 9 | 10 | # ERROR Unclosed StringLiteral 11 | err03 = { "str 12 | 13 | # ERROR Missing newline after ->. 14 | err04 = { $sel -> } 15 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.ftl: -------------------------------------------------------------------------------- 1 | ### BE CAREFUL WHEN EDITING THIS FILE 2 | ### 3 | ### The last character in this file is the dash ("-") on line 8. 4 | ### We want to test a literal which starts like a negative number. 5 | ### Most editors automatically add a trailing newline at EOF. 6 | ### If you edit this file make sure to turn this behavior off. 7 | 8 | key1 = {- -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/dash_at_eof.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "BE CAREFUL WHEN EDITING THIS FILE\n\nThe last character in this file is the dash (\"-\") on line 8.\nWe want to test a literal which starts like a negative number.\nMost editors automatically add a trailing newline at EOF.\nIf you edit this file make sure to turn this behavior off.", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 298 11 | } 12 | }, 13 | { 14 | "type": "Junk", 15 | "annotations": [ 16 | { 17 | "type": "Annotation", 18 | "code": "E0004", 19 | "arguments": [ 20 | "a-zA-Z" 21 | ], 22 | "message": "Expected a character from range: \"a-zA-Z\"", 23 | "span": { 24 | "type": "Span", 25 | "start": 309, 26 | "end": 309 27 | } 28 | } 29 | ], 30 | "content": "key1 = {-", 31 | "span": { 32 | "type": "Span", 33 | "start": 300, 34 | "end": 309 35 | } 36 | } 37 | ], 38 | "span": { 39 | "type": "Span", 40 | "start": 0, 41 | "end": 309 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/elements_indent.ftl: -------------------------------------------------------------------------------- 1 | foo = Foo 2 | .attr = Foo Attr 3 | 4 | bar = Bar 5 | .attr1 = Bar Attr 1 6 | .attr2 = Bar Attr 2 7 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/empty_resource.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectfluent/python-fluent/e4fc3c20e632babfa70d9cc6b8eaf4141c2aa83f/fluent.syntax/tests/syntax/fixtures_structure/empty_resource.ftl -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/empty_resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [], 4 | "span": { 5 | "type": "Span", 6 | "start": 0, 7 | "end": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/empty_resource_with_ws.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/empty_resource_with_ws.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [], 4 | "span": { 5 | "type": "Span", 6 | "start": 0, 7 | "end": 10 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/escape_sequences.ftl: -------------------------------------------------------------------------------- 1 | ## Literal text 2 | text-backslash-one = Value with \ a backslash 3 | text-backslash-two = Value with \\ two backslashes 4 | text-backslash-brace = Value with \{placeable} 5 | text-backslash-u = \u0041 6 | text-backslash-backslash-u = \\u0041 7 | 8 | ## String literals 9 | quote-in-string = {"\""} 10 | backslash-in-string = {"\\"} 11 | # ERROR Mismatched quote 12 | mismatched-quote = {"\\""} 13 | # ERROR Unknown escape 14 | unknown-escape = {"\x"} 15 | 16 | ## Unicode escapes 17 | string-unicode-sequence = {"\u0041"} 18 | string-escaped-unicode = {"\\u0041"} 19 | # ERROR Unknown escape 20 | unknown-unicode = {"\u000z"} 21 | 22 | ## Literal braces 23 | brace-open = An opening {"{"} brace. 24 | brace-close = A closing {"}"} brace. 25 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/expressions_call_args.ftl: -------------------------------------------------------------------------------- 1 | key = { FOO(arg1: 1, 2 | arg2: 2) } 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/indent.ftl: -------------------------------------------------------------------------------- 1 | err01 = A 2 | 3 | key2 = { 4 | a } 5 | 6 | key3 = { a 7 | } 8 | 9 | key4 = { 10 | { a }} 11 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/junk.ftl: -------------------------------------------------------------------------------- 1 | err01 = {1xx} 2 | err02 = {1xx} 3 | 4 | err03 = {1xx} 5 | 1xx 6 | 7 | err04 = {1xx} 8 | 9 | 1xx 10 | 11 | err05 = { 12 | 13 | 1xx 14 | 15 | err06 = {1xx 16 | 17 | .attr = Value 18 | 19 | err07 = { 20 | 21 | key08 = Value 22 | 23 | err09 = { 24 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/leading_dots.ftl: -------------------------------------------------------------------------------- 1 | key01 = .Value 2 | key02 = …Value 3 | key03 = {"."}Value 4 | key04 = 5 | {"."}Value 6 | 7 | key05 = Value 8 | {"."}Continued 9 | 10 | key06 = .Value 11 | {"."}Continued 12 | 13 | # ERROR (attr .Continued must have a value) 14 | key07 = Value 15 | .Continued 16 | 17 | # ERROR (attr .Value must have a value) 18 | key08 = 19 | .Value 20 | 21 | # ERROR (attr .Value must have a value) 22 | key09 = 23 | .Value 24 | Continued 25 | 26 | key10 = 27 | .Value = which looks like an attribute 28 | Continued 29 | 30 | key11 = 31 | {"."}Value = which looks like an attribute 32 | Continued 33 | 34 | key12 = 35 | .accesskey = 36 | A 37 | 38 | key13 = 39 | .attribute = .Value 40 | 41 | key14 = 42 | .attribute = 43 | {"."}Value 44 | 45 | key15 = 46 | { 1 -> 47 | [one] .Value 48 | *[other] 49 | {"."}Value 50 | } 51 | 52 | # ERROR (variant must have a value) 53 | key16 = 54 | { 1 -> 55 | *[one] 56 | .Value 57 | } 58 | 59 | # ERROR (unclosed placeable) 60 | key17 = 61 | { 1 -> 62 | *[one] Value 63 | .Continued 64 | } 65 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/leading_empty_lines.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | key01 = Value 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/leading_empty_lines.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key01", 9 | "span": { 10 | "type": "Span", 11 | "start": 2, 12 | "end": 7 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 10, 24 | "end": 15 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 10, 31 | "end": 15 32 | } 33 | }, 34 | "attributes": [], 35 | "comment": null, 36 | "span": { 37 | "type": "Span", 38 | "start": 2, 39 | "end": 15 40 | } 41 | } 42 | ], 43 | "span": { 44 | "type": "Span", 45 | "start": 0, 46 | "end": 16 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/leading_empty_lines_with_ws.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | key01 = Value 6 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/leading_empty_lines_with_ws.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key01", 9 | "span": { 10 | "type": "Span", 11 | "start": 10, 12 | "end": 15 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 18, 24 | "end": 23 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 18, 31 | "end": 23 32 | } 33 | }, 34 | "attributes": [], 35 | "comment": null, 36 | "span": { 37 | "type": "Span", 38 | "start": 10, 39 | "end": 23 40 | } 41 | } 42 | ], 43 | "span": { 44 | "type": "Span", 45 | "start": 0, 46 | "end": 24 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/message_reference_as_selector.ftl: -------------------------------------------------------------------------------- 1 | err1 = 2 | { foo -> 3 | *[1] One 4 | } 5 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/message_reference_as_selector.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0016", 10 | "arguments": [], 11 | "message": "Message references cannot be used as selectors", 12 | "span": { 13 | "type": "Span", 14 | "start": 17, 15 | "end": 17 16 | } 17 | } 18 | ], 19 | "content": "err1 =\n { foo ->\n *[1] One\n }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 42 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 42 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.ftl: -------------------------------------------------------------------------------- 1 | ### BE CAREFUL WHEN EDITING THIS FILE 2 | ### 3 | ### The last character in this file is the space (" ") on line 14. 4 | ### We want to test a message with no value and with no EOL at the 5 | ### end of the fie. Most editors automatically add a trailing newline 6 | ### at EOF. If you edit this file make sure to turn this behavior off. 7 | 8 | key1 = 9 | 10 | 11 | 12 | key2 = 13 | 14 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_multiline_pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "BE CAREFUL WHEN EDITING THIS FILE\n\nThe last character in this file is the space (\" \") on line 14.\nWe want to test a message with no value and with no EOL at the\nend of the fie. Most editors automatically add a trailing newline\nat EOF. If you edit this file make sure to turn this behavior off.", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 316 11 | } 12 | }, 13 | { 14 | "type": "Junk", 15 | "annotations": [ 16 | { 17 | "type": "Annotation", 18 | "code": "E0005", 19 | "arguments": [ 20 | "key1" 21 | ], 22 | "message": "Expected message \"key1\" to have a value or attributes", 23 | "span": { 24 | "type": "Span", 25 | "start": 324, 26 | "end": 324 27 | } 28 | } 29 | ], 30 | "content": "key1 =\n\n \n\n", 31 | "span": { 32 | "type": "Span", 33 | "start": 318, 34 | "end": 329 35 | } 36 | }, 37 | { 38 | "type": "Junk", 39 | "annotations": [ 40 | { 41 | "type": "Annotation", 42 | "code": "E0005", 43 | "arguments": [ 44 | "key2" 45 | ], 46 | "message": "Expected message \"key2\" to have a value or attributes", 47 | "span": { 48 | "type": "Span", 49 | "start": 335, 50 | "end": 335 51 | } 52 | } 53 | ], 54 | "content": "key2 =\n\n ", 55 | "span": { 56 | "type": "Span", 57 | "start": 329, 58 | "end": 338 59 | } 60 | } 61 | ], 62 | "span": { 63 | "type": "Span", 64 | "start": 0, 65 | "end": 338 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/message_with_empty_pattern.ftl: -------------------------------------------------------------------------------- 1 | ### BE CAREFUL WHEN EDITING THIS FILE 2 | ### 3 | ### The last character in this file is the equals sign in `key5 =` in line 18. 4 | ### We want to test a message with no value and with no EOL after its 5 | ### identifier. Most editors automatically add a trailing newline at EOF. 6 | ### If you edit this file make sure to turn this behavior off. 7 | 8 | key1 = 9 | 10 | key2 = 11 | 12 | key3 = 13 | .attr = Attr 14 | 15 | key4 = 16 | .attr = Attr 17 | 18 | key5 = -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.ftl: -------------------------------------------------------------------------------- 1 | 2 | # This is 3 | # 4 | # An example of a multiline comment 5 | 6 | key = Value 7 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Comment", 6 | "content": "This is\n\nAn example of a multiline comment", 7 | "span": { 8 | "type": "Span", 9 | "start": 1, 10 | "end": 48 11 | } 12 | }, 13 | { 14 | "type": "Message", 15 | "id": { 16 | "type": "Identifier", 17 | "name": "key", 18 | "span": { 19 | "type": "Span", 20 | "start": 50, 21 | "end": 53 22 | } 23 | }, 24 | "value": { 25 | "type": "Pattern", 26 | "elements": [ 27 | { 28 | "type": "TextElement", 29 | "value": "Value", 30 | "span": { 31 | "type": "Span", 32 | "start": 56, 33 | "end": 61 34 | } 35 | } 36 | ], 37 | "span": { 38 | "type": "Span", 39 | "start": 56, 40 | "end": 61 41 | } 42 | }, 43 | "attributes": [], 44 | "comment": null, 45 | "span": { 46 | "type": "Span", 47 | "start": 50, 48 | "end": 61 49 | } 50 | } 51 | ], 52 | "span": { 53 | "type": "Span", 54 | "start": 0, 55 | "end": 62 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_pattern.ftl: -------------------------------------------------------------------------------- 1 | key01 = Value 2 | Continued here. 3 | 4 | key02 = 5 | Value 6 | Continued here. 7 | 8 | # ERROR "Continued" looks like a new message. 9 | # key03 parses fine with just "Value". 10 | key03 = 11 | Value 12 | Continued here 13 | and here. 14 | 15 | # ERROR "Continued" and "and" look like new messages 16 | # key04 parses fine with just "Value". 17 | key04 = 18 | Value 19 | Continued here 20 | and even here. 21 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_string.ftl: -------------------------------------------------------------------------------- 1 | err01 = { BUILTIN(key: " 2 | text 3 | ") } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0020", 10 | "arguments": [], 11 | "message": "Unterminated string expression", 12 | "span": { 13 | "type": "Span", 14 | "start": 24, 15 | "end": 24 16 | } 17 | } 18 | ], 19 | "content": "err01 = { BUILTIN(key: \"\n text\n \") }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 40 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 40 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_with_non_empty_first_line.ftl: -------------------------------------------------------------------------------- 1 | key = Value 2 | Value 2 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_with_non_empty_first_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value\nValue 2", 21 | "span": { 22 | "type": "Span", 23 | "start": 6, 24 | "end": 23 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 6, 31 | "end": 23 32 | } 33 | }, 34 | "attributes": [], 35 | "comment": null, 36 | "span": { 37 | "type": "Span", 38 | "start": 0, 39 | "end": 23 40 | } 41 | } 42 | ], 43 | "span": { 44 | "type": "Span", 45 | "start": 0, 46 | "end": 24 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_with_placeables.ftl: -------------------------------------------------------------------------------- 1 | key = 2 | Foo { bar } 3 | Baz 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/multiline_with_placeables.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Foo ", 21 | "span": { 22 | "type": "Span", 23 | "start": 10, 24 | "end": 14 25 | } 26 | }, 27 | { 28 | "type": "Placeable", 29 | "expression": { 30 | "type": "MessageReference", 31 | "id": { 32 | "type": "Identifier", 33 | "name": "bar", 34 | "span": { 35 | "type": "Span", 36 | "start": 16, 37 | "end": 19 38 | } 39 | }, 40 | "attribute": null, 41 | "span": { 42 | "type": "Span", 43 | "start": 16, 44 | "end": 19 45 | } 46 | }, 47 | "span": { 48 | "type": "Span", 49 | "start": 14, 50 | "end": 21 51 | } 52 | }, 53 | { 54 | "type": "TextElement", 55 | "value": "\nBaz", 56 | "span": { 57 | "type": "Span", 58 | "start": 21, 59 | "end": 29 60 | } 61 | } 62 | ], 63 | "span": { 64 | "type": "Span", 65 | "start": 6, 66 | "end": 29 67 | } 68 | }, 69 | "attributes": [], 70 | "comment": null, 71 | "span": { 72 | "type": "Span", 73 | "start": 0, 74 | "end": 29 75 | } 76 | } 77 | ], 78 | "span": { 79 | "type": "Span", 80 | "start": 0, 81 | "end": 30 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/non_id_attribute_name.ftl: -------------------------------------------------------------------------------- 1 | err01 = Value 2 | .2 = Foo 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/non_id_attribute_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0004", 10 | "arguments": [ 11 | "a-zA-Z" 12 | ], 13 | "message": "Expected a character from range: \"a-zA-Z\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 19, 17 | "end": 19 18 | } 19 | } 20 | ], 21 | "content": "err01 = Value\n .2 = Foo\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 27 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 27 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/placeable_at_eol.ftl: -------------------------------------------------------------------------------- 1 | key1 = 2 | A multiline message with a { placeable } 3 | at the end of line. The message should 4 | consist of three lines of text. 5 | 6 | key2 = 7 | A multiline message with a { placeable } 8 | 9 | key3 = A singleline message with a { placeable } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/placeable_at_line_extremes.ftl: -------------------------------------------------------------------------------- 1 | key1 = 2 | { foo } 3 | 4 | key2 = 5 | Foo { foo } 6 | 7 | key3 = 8 | { foo } Foo 9 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/placeable_in_placeable.ftl: -------------------------------------------------------------------------------- 1 | key1 = {{ foo }} 2 | 3 | key2 = { { foo } } 4 | 5 | key3 = 6 | { 7 | { foo } 8 | } 9 | 10 | err1 = { { foo } 11 | 12 | err2 = { foo } } 13 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/placeable_without_close_bracket.ftl: -------------------------------------------------------------------------------- 1 | err01 = { $num 2 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/placeable_without_close_bracket.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "}" 12 | ], 13 | "message": "Expected token: \"}\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 15, 17 | "end": 15 18 | } 19 | } 20 | ], 21 | "content": "err01 = { $num\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 15 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 15 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/resource_comment.ftl: -------------------------------------------------------------------------------- 1 | ### This is a resource wide comment 2 | ### It's multiline 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/resource_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "This is a resource wide comment\nIt's multiline", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 54 11 | } 12 | } 13 | ], 14 | "span": { 15 | "type": "Span", 16 | "start": 0, 17 | "end": 55 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.ftl: -------------------------------------------------------------------------------- 1 | ### This is a comment 2 | ### This comment is multiline 3 | ### 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/resource_comment_trailing_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "ResourceComment", 6 | "content": "This is a comment\nThis comment is multiline\n", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 55 11 | } 12 | } 13 | ], 14 | "span": { 15 | "type": "Span", 16 | "start": 0, 17 | "end": 56 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/second_attribute_starts_from_nl.ftl: -------------------------------------------------------------------------------- 1 | key = Value 2 | .label = Value 3 | .accesskey = K 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/second_attribute_starts_from_nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "key", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 6, 24 | "end": 11 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 6, 31 | "end": 11 32 | } 33 | }, 34 | "attributes": [ 35 | { 36 | "type": "Attribute", 37 | "id": { 38 | "type": "Identifier", 39 | "name": "label", 40 | "span": { 41 | "type": "Span", 42 | "start": 17, 43 | "end": 22 44 | } 45 | }, 46 | "value": { 47 | "type": "Pattern", 48 | "elements": [ 49 | { 50 | "type": "TextElement", 51 | "value": "Value", 52 | "span": { 53 | "type": "Span", 54 | "start": 25, 55 | "end": 30 56 | } 57 | } 58 | ], 59 | "span": { 60 | "type": "Span", 61 | "start": 25, 62 | "end": 30 63 | } 64 | }, 65 | "span": { 66 | "type": "Span", 67 | "start": 16, 68 | "end": 30 69 | } 70 | }, 71 | { 72 | "type": "Attribute", 73 | "id": { 74 | "type": "Identifier", 75 | "name": "accesskey", 76 | "span": { 77 | "type": "Span", 78 | "start": 32, 79 | "end": 41 80 | } 81 | }, 82 | "value": { 83 | "type": "Pattern", 84 | "elements": [ 85 | { 86 | "type": "TextElement", 87 | "value": "K", 88 | "span": { 89 | "type": "Span", 90 | "start": 44, 91 | "end": 45 92 | } 93 | } 94 | ], 95 | "span": { 96 | "type": "Span", 97 | "start": 44, 98 | "end": 45 99 | } 100 | }, 101 | "span": { 102 | "type": "Span", 103 | "start": 31, 104 | "end": 45 105 | } 106 | } 107 | ], 108 | "comment": null, 109 | "span": { 110 | "type": "Span", 111 | "start": 0, 112 | "end": 45 113 | } 114 | } 115 | ], 116 | "span": { 117 | "type": "Span", 118 | "start": 0, 119 | "end": 46 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_with_two_selectors.ftl: -------------------------------------------------------------------------------- 1 | err01 = { $foo $faa } 2 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_with_two_selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "}" 12 | ], 13 | "message": "Expected token: \"}\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 15, 17 | "end": 15 18 | } 19 | } 20 | ], 21 | "content": "err01 = { $foo $faa }\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 22 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 22 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_without_arrow.ftl: -------------------------------------------------------------------------------- 1 | err01 = { $foo - } 2 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_without_arrow.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "}" 12 | ], 13 | "message": "Expected token: \"}\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 15, 17 | "end": 15 18 | } 19 | } 20 | ], 21 | "content": "err01 = { $foo - }\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 19 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 19 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_without_variants.ftl: -------------------------------------------------------------------------------- 1 | err01 = { $foo -> } 2 | 3 | err02 = { $foo -> 4 | } 5 | 6 | err03 = { $foo -> 7 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expression_without_variants.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "␤" 12 | ], 13 | "message": "Expected token: \"␤\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 18, 17 | "end": 18 18 | } 19 | } 20 | ], 21 | "content": "err01 = { $foo -> }\n\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 21 26 | } 27 | }, 28 | { 29 | "type": "Junk", 30 | "annotations": [ 31 | { 32 | "type": "Annotation", 33 | "code": "E0011", 34 | "arguments": [], 35 | "message": "Expected at least one variant after \"->\"", 36 | "span": { 37 | "type": "Span", 38 | "start": 43, 39 | "end": 43 40 | } 41 | } 42 | ], 43 | "content": "err02 = { $foo ->\n }\n\n", 44 | "span": { 45 | "type": "Span", 46 | "start": 21, 47 | "end": 46 48 | } 49 | }, 50 | { 51 | "type": "Junk", 52 | "annotations": [ 53 | { 54 | "type": "Annotation", 55 | "code": "E0011", 56 | "arguments": [], 57 | "message": "Expected at least one variant after \"->\"", 58 | "span": { 59 | "type": "Span", 60 | "start": 64, 61 | "end": 64 62 | } 63 | } 64 | ], 65 | "content": "err03 = { $foo ->\n", 66 | "span": { 67 | "type": "Span", 68 | "start": 46, 69 | "end": 64 70 | } 71 | } 72 | ], 73 | "span": { 74 | "type": "Span", 75 | "start": 0, 76 | "end": 64 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expressions.ftl: -------------------------------------------------------------------------------- 1 | # ERROR No blanks are allowed between * and [. 2 | err01 = { $sel -> 3 | * [key] Value 4 | } 5 | 6 | # ERROR Missing default variant. 7 | err02 = { $sel -> 8 | [key] Value 9 | } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/select_expressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Comment", 6 | "content": "ERROR No blanks are allowed between * and [.", 7 | "span": { 8 | "type": "Span", 9 | "start": 0, 10 | "end": 46 11 | } 12 | }, 13 | { 14 | "type": "Junk", 15 | "annotations": [ 16 | { 17 | "type": "Annotation", 18 | "code": "E0011", 19 | "arguments": [], 20 | "message": "Expected at least one variant after \"->\"", 21 | "span": { 22 | "type": "Span", 23 | "start": 69, 24 | "end": 69 25 | } 26 | } 27 | ], 28 | "content": "err01 = { $sel ->\n * [key] Value\n}\n\n", 29 | "span": { 30 | "type": "Span", 31 | "start": 47, 32 | "end": 87 33 | } 34 | }, 35 | { 36 | "type": "Comment", 37 | "content": "ERROR Missing default variant.", 38 | "span": { 39 | "type": "Span", 40 | "start": 87, 41 | "end": 119 42 | } 43 | }, 44 | { 45 | "type": "Junk", 46 | "annotations": [ 47 | { 48 | "type": "Annotation", 49 | "code": "E0010", 50 | "arguments": [], 51 | "message": "Expected one of the variants to be marked as default (*)", 52 | "span": { 53 | "type": "Span", 54 | "start": 154, 55 | "end": 154 56 | } 57 | } 58 | ], 59 | "content": "err02 = { $sel ->\n [key] Value\n}\n", 60 | "span": { 61 | "type": "Span", 62 | "start": 120, 63 | "end": 156 64 | } 65 | } 66 | ], 67 | "span": { 68 | "type": "Span", 69 | "start": 0, 70 | "end": 156 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/simple_message.ftl: -------------------------------------------------------------------------------- 1 | foo = Foo 2 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/simple_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "foo", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Foo", 21 | "span": { 22 | "type": "Span", 23 | "start": 6, 24 | "end": 9 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 6, 31 | "end": 9 32 | } 33 | }, 34 | "attributes": [], 35 | "comment": null, 36 | "span": { 37 | "type": "Span", 38 | "start": 0, 39 | "end": 9 40 | } 41 | } 42 | ], 43 | "span": { 44 | "type": "Span", 45 | "start": 0, 46 | "end": 10 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/single_char_id.ftl: -------------------------------------------------------------------------------- 1 | k = Value 2 | .l = Foo 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/single_char_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "k", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 1 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 4, 24 | "end": 9 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 4, 31 | "end": 9 32 | } 33 | }, 34 | "attributes": [ 35 | { 36 | "type": "Attribute", 37 | "id": { 38 | "type": "Identifier", 39 | "name": "l", 40 | "span": { 41 | "type": "Span", 42 | "start": 15, 43 | "end": 16 44 | } 45 | }, 46 | "value": { 47 | "type": "Pattern", 48 | "elements": [ 49 | { 50 | "type": "TextElement", 51 | "value": "Foo", 52 | "span": { 53 | "type": "Span", 54 | "start": 19, 55 | "end": 22 56 | } 57 | } 58 | ], 59 | "span": { 60 | "type": "Span", 61 | "start": 19, 62 | "end": 22 63 | } 64 | }, 65 | "span": { 66 | "type": "Span", 67 | "start": 14, 68 | "end": 22 69 | } 70 | } 71 | ], 72 | "comment": null, 73 | "span": { 74 | "type": "Span", 75 | "start": 0, 76 | "end": 22 77 | } 78 | } 79 | ], 80 | "span": { 81 | "type": "Span", 82 | "start": 0, 83 | "end": 23 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/sparse-messages.ftl: -------------------------------------------------------------------------------- 1 | key = 2 | 3 | 4 | Value 5 | 6 | key2 = 7 | 8 | 9 | .attr = Attribute 10 | 11 | 12 | key3 = 13 | Value 14 | Value2 15 | 16 | 17 | Value 4 18 | Value3 19 | 20 | 21 | 22 | .attr2 = Attr 2 23 | 24 | 25 | key5 = Value 5 26 | 27 | key6 = { $sel -> 28 | 29 | [one] One 30 | 31 | *[two] Two 32 | 33 | } 34 | 35 | key8 = 36 | 37 | { 38 | 39 | $sel 40 | 41 | -> 42 | 43 | *[one] One 44 | 45 | } 46 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.ftl: -------------------------------------------------------------------------------- 1 | foo = Value 2 | 3 | # This is a standalone comment 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/standalone_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Message", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "foo", 9 | "span": { 10 | "type": "Span", 11 | "start": 0, 12 | "end": 3 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "TextElement", 20 | "value": "Value", 21 | "span": { 22 | "type": "Span", 23 | "start": 6, 24 | "end": 11 25 | } 26 | } 27 | ], 28 | "span": { 29 | "type": "Span", 30 | "start": 6, 31 | "end": 11 32 | } 33 | }, 34 | "attributes": [], 35 | "comment": null, 36 | "span": { 37 | "type": "Span", 38 | "start": 0, 39 | "end": 11 40 | } 41 | }, 42 | { 43 | "type": "Comment", 44 | "content": "This is a standalone comment", 45 | "span": { 46 | "type": "Span", 47 | "start": 13, 48 | "end": 43 49 | } 50 | } 51 | ], 52 | "span": { 53 | "type": "Span", 54 | "start": 0, 55 | "end": 44 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/standalone_identifier.ftl: -------------------------------------------------------------------------------- 1 | foo 2 | # ~ERROR E0003, pos 3, args "=" 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/standalone_identifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "=" 12 | ], 13 | "message": "Expected token: \"=\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 3, 17 | "end": 3 18 | } 19 | } 20 | ], 21 | "content": "foo\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 4 26 | } 27 | }, 28 | { 29 | "type": "Comment", 30 | "content": "~ERROR E0003, pos 3, args \"=\"", 31 | "span": { 32 | "type": "Span", 33 | "start": 4, 34 | "end": 35 35 | } 36 | } 37 | ], 38 | "span": { 39 | "type": "Span", 40 | "start": 0, 41 | "end": 36 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/term.ftl: -------------------------------------------------------------------------------- 1 | -term = 2 | { $case -> 3 | *[uppercase] Term 4 | [lowercase] term 5 | } 6 | .attr = a 7 | 8 | key01 = {-term} 9 | key02 = {-term()} 10 | key03 = {-term(case: "uppercase")} 11 | 12 | 13 | key04 = 14 | { -term.attr -> 15 | [a] { -term } A 16 | [b] { -term() } B 17 | *[x] X 18 | } 19 | 20 | -err1 = 21 | -err2 = 22 | .attr = Attribute 23 | --err3 = Error 24 | err4 = { --err4 } 25 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.ftl: -------------------------------------------------------------------------------- 1 | -foo = 2 | .attr = Attribute 3 | 4 | -bar = 5 | 6 | -baz 7 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/term_with_empty_pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0006", 10 | "arguments": [ 11 | "foo" 12 | ], 13 | "message": "Expected term \"-foo\" to have a value", 14 | "span": { 15 | "type": "Span", 16 | "start": 6, 17 | "end": 6 18 | } 19 | } 20 | ], 21 | "content": "-foo =\n .attr = Attribute\n\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 30 26 | } 27 | }, 28 | { 29 | "type": "Junk", 30 | "annotations": [ 31 | { 32 | "type": "Annotation", 33 | "code": "E0006", 34 | "arguments": [ 35 | "bar" 36 | ], 37 | "message": "Expected term \"-bar\" to have a value", 38 | "span": { 39 | "type": "Span", 40 | "start": 36, 41 | "end": 36 42 | } 43 | } 44 | ], 45 | "content": "-bar =\n\n", 46 | "span": { 47 | "type": "Span", 48 | "start": 30, 49 | "end": 38 50 | } 51 | }, 52 | { 53 | "type": "Junk", 54 | "annotations": [ 55 | { 56 | "type": "Annotation", 57 | "code": "E0003", 58 | "arguments": [ 59 | "=" 60 | ], 61 | "message": "Expected token: \"=\"", 62 | "span": { 63 | "type": "Span", 64 | "start": 42, 65 | "end": 42 66 | } 67 | } 68 | ], 69 | "content": "-baz\n", 70 | "span": { 71 | "type": "Span", 72 | "start": 38, 73 | "end": 43 74 | } 75 | } 76 | ], 77 | "span": { 78 | "type": "Span", 79 | "start": 0, 80 | "end": 43 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/unclosed.ftl: -------------------------------------------------------------------------------- 1 | err01 = { 2 | key02 = Value 02 3 | 4 | err03 = { 5 | FUNC( 6 | arg 7 | , 8 | namedArg: "Value" 9 | , 10 | key04 = Value 04 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/unclosed_empty_placeable_error.ftl: -------------------------------------------------------------------------------- 1 | err01 = { 2 | key01 = Bar 3 | err02 = { 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/unclosed_empty_placeable_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "}" 12 | ], 13 | "message": "Expected token: \"}\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 10, 17 | "end": 10 18 | } 19 | } 20 | ], 21 | "content": "err01 = {\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 10 26 | } 27 | }, 28 | { 29 | "type": "Message", 30 | "id": { 31 | "type": "Identifier", 32 | "name": "key01", 33 | "span": { 34 | "type": "Span", 35 | "start": 10, 36 | "end": 15 37 | } 38 | }, 39 | "value": { 40 | "type": "Pattern", 41 | "elements": [ 42 | { 43 | "type": "TextElement", 44 | "value": "Bar", 45 | "span": { 46 | "type": "Span", 47 | "start": 18, 48 | "end": 21 49 | } 50 | } 51 | ], 52 | "span": { 53 | "type": "Span", 54 | "start": 18, 55 | "end": 21 56 | } 57 | }, 58 | "attributes": [], 59 | "comment": null, 60 | "span": { 61 | "type": "Span", 62 | "start": 10, 63 | "end": 21 64 | } 65 | }, 66 | { 67 | "type": "Junk", 68 | "annotations": [ 69 | { 70 | "type": "Annotation", 71 | "code": "E0028", 72 | "arguments": [], 73 | "message": "Expected an inline expression", 74 | "span": { 75 | "type": "Span", 76 | "start": 32, 77 | "end": 32 78 | } 79 | } 80 | ], 81 | "content": "err02 = {\n", 82 | "span": { 83 | "type": "Span", 84 | "start": 22, 85 | "end": 32 86 | } 87 | } 88 | ], 89 | "span": { 90 | "type": "Span", 91 | "start": 0, 92 | "end": 32 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/unknown_entry_start.ftl: -------------------------------------------------------------------------------- 1 | 2 | 8err = Foo 3 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/unknown_entry_start.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0002", 10 | "arguments": [], 11 | "message": "Expected an entry start", 12 | "span": { 13 | "type": "Span", 14 | "start": 1, 15 | "end": 1 16 | } 17 | } 18 | ], 19 | "content": "8err = Foo\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 1, 23 | "end": 12 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 12 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_ends_abruptly.ftl: -------------------------------------------------------------------------------- 1 | key = { $foo -> 2 | *[ 3 | # ~ERROR E0013, pos 23 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_ends_abruptly.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0004", 10 | "arguments": [ 11 | "a-zA-Z" 12 | ], 13 | "message": "Expected a character from range: \"a-zA-Z\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 23, 17 | "end": 23 18 | } 19 | } 20 | ], 21 | "content": "key = { $foo ->\n *[\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 23 26 | } 27 | }, 28 | { 29 | "type": "Comment", 30 | "content": "~ERROR E0013, pos 23", 31 | "span": { 32 | "type": "Span", 33 | "start": 23, 34 | "end": 45 35 | } 36 | } 37 | ], 38 | "span": { 39 | "type": "Span", 40 | "start": 0, 41 | "end": 46 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_keys.ftl: -------------------------------------------------------------------------------- 1 | key01 = { $sel -> 2 | *[ 3 | key 4 | ] Value 5 | } 6 | 7 | key02 = { $sel -> 8 | *[ 9 | key 10 | ] 11 | 12 | Value 13 | } 14 | 15 | err01 = { $sel -> 16 | *["key"] Value 17 | } 18 | 19 | err02 = { $sel -> 20 | *[-key] Value 21 | } 22 | 23 | err03 = { $sel -> 24 | *[-key.attr] Value 25 | } 26 | 27 | err04 = { $sel -> 28 | *[-key()] Value 29 | } 30 | 31 | err05 = { $sel -> 32 | *[-key.attr()] Value 33 | } 34 | 35 | err06 = { $sel -> 36 | *[key.attr] Value 37 | } 38 | 39 | err07 = { $sel -> 40 | *[$key] Value 41 | } 42 | 43 | err08 = { $sel -> 44 | *[FUNC()] Value 45 | } 46 | 47 | err09 = { $sel -> 48 | *[{key}] Value 49 | } 50 | 51 | err10 = { $sel -> 52 | *[{"key"}] Value 53 | } 54 | 55 | err11 = { $sel -> 56 | *[{3.14}] Value 57 | } 58 | 59 | err12 = { $sel -> 60 | *[{$key}] Value 61 | } 62 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_starts_from_nl.ftl: -------------------------------------------------------------------------------- 1 | -term = { $sel -> 2 | *[one] Value 3 | } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_starts_from_nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Term", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "term", 9 | "span": { 10 | "type": "Span", 11 | "start": 1, 12 | "end": 5 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "Placeable", 20 | "expression": { 21 | "type": "SelectExpression", 22 | "selector": { 23 | "type": "VariableReference", 24 | "id": { 25 | "type": "Identifier", 26 | "name": "sel", 27 | "span": { 28 | "type": "Span", 29 | "start": 11, 30 | "end": 14 31 | } 32 | }, 33 | "span": { 34 | "type": "Span", 35 | "start": 10, 36 | "end": 14 37 | } 38 | }, 39 | "variants": [ 40 | { 41 | "type": "Variant", 42 | "key": { 43 | "type": "Identifier", 44 | "name": "one", 45 | "span": { 46 | "type": "Span", 47 | "start": 20, 48 | "end": 23 49 | } 50 | }, 51 | "value": { 52 | "type": "Pattern", 53 | "elements": [ 54 | { 55 | "type": "TextElement", 56 | "value": "Value", 57 | "span": { 58 | "type": "Span", 59 | "start": 25, 60 | "end": 30 61 | } 62 | } 63 | ], 64 | "span": { 65 | "type": "Span", 66 | "start": 25, 67 | "end": 30 68 | } 69 | }, 70 | "default": true, 71 | "span": { 72 | "type": "Span", 73 | "start": 18, 74 | "end": 30 75 | } 76 | } 77 | ], 78 | "span": { 79 | "type": "Span", 80 | "start": 10, 81 | "end": 35 82 | } 83 | }, 84 | "span": { 85 | "type": "Span", 86 | "start": 8, 87 | "end": 36 88 | } 89 | } 90 | ], 91 | "span": { 92 | "type": "Span", 93 | "start": 8, 94 | "end": 36 95 | } 96 | }, 97 | "attributes": [], 98 | "comment": null, 99 | "span": { 100 | "type": "Span", 101 | "start": 0, 102 | "end": 36 103 | } 104 | } 105 | ], 106 | "span": { 107 | "type": "Span", 108 | "start": 0, 109 | "end": 37 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_digit_key.ftl: -------------------------------------------------------------------------------- 1 | -term = { $sel -> 2 | *[-2] Foo 3 | } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_digit_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Term", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "term", 9 | "span": { 10 | "type": "Span", 11 | "start": 1, 12 | "end": 5 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "Placeable", 20 | "expression": { 21 | "type": "SelectExpression", 22 | "selector": { 23 | "type": "VariableReference", 24 | "id": { 25 | "type": "Identifier", 26 | "name": "sel", 27 | "span": { 28 | "type": "Span", 29 | "start": 11, 30 | "end": 14 31 | } 32 | }, 33 | "span": { 34 | "type": "Span", 35 | "start": 10, 36 | "end": 14 37 | } 38 | }, 39 | "variants": [ 40 | { 41 | "type": "Variant", 42 | "key": { 43 | "value": "-2", 44 | "type": "NumberLiteral", 45 | "span": { 46 | "type": "Span", 47 | "start": 28, 48 | "end": 30 49 | } 50 | }, 51 | "value": { 52 | "type": "Pattern", 53 | "elements": [ 54 | { 55 | "type": "TextElement", 56 | "value": "Foo", 57 | "span": { 58 | "type": "Span", 59 | "start": 32, 60 | "end": 35 61 | } 62 | } 63 | ], 64 | "span": { 65 | "type": "Span", 66 | "start": 32, 67 | "end": 35 68 | } 69 | }, 70 | "default": true, 71 | "span": { 72 | "type": "Span", 73 | "start": 26, 74 | "end": 35 75 | } 76 | } 77 | ], 78 | "span": { 79 | "type": "Span", 80 | "start": 10, 81 | "end": 40 82 | } 83 | }, 84 | "span": { 85 | "type": "Span", 86 | "start": 8, 87 | "end": 41 88 | } 89 | } 90 | ], 91 | "span": { 92 | "type": "Span", 93 | "start": 8, 94 | "end": 41 95 | } 96 | }, 97 | "attributes": [], 98 | "comment": null, 99 | "span": { 100 | "type": "Span", 101 | "start": 0, 102 | "end": 41 103 | } 104 | } 105 | ], 106 | "span": { 107 | "type": "Span", 108 | "start": 0, 109 | "end": 42 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_empty_pattern.ftl: -------------------------------------------------------------------------------- 1 | key1 = 2 | { 1 -> 3 | *[one] {""} 4 | } 5 | 6 | err2 = 7 | { $sel -> 8 | *[one] 9 | } 10 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_leading_space_in_name.ftl: -------------------------------------------------------------------------------- 1 | -term = { $sel -> 2 | *[ one] Foo 3 | } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_leading_space_in_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Term", 6 | "id": { 7 | "type": "Identifier", 8 | "name": "term", 9 | "span": { 10 | "type": "Span", 11 | "start": 1, 12 | "end": 5 13 | } 14 | }, 15 | "value": { 16 | "type": "Pattern", 17 | "elements": [ 18 | { 19 | "type": "Placeable", 20 | "expression": { 21 | "type": "SelectExpression", 22 | "selector": { 23 | "type": "VariableReference", 24 | "id": { 25 | "type": "Identifier", 26 | "name": "sel", 27 | "span": { 28 | "type": "Span", 29 | "start": 11, 30 | "end": 14 31 | } 32 | }, 33 | "span": { 34 | "type": "Span", 35 | "start": 10, 36 | "end": 14 37 | } 38 | }, 39 | "variants": [ 40 | { 41 | "type": "Variant", 42 | "key": { 43 | "type": "Identifier", 44 | "name": "one", 45 | "span": { 46 | "type": "Span", 47 | "start": 30, 48 | "end": 33 49 | } 50 | }, 51 | "value": { 52 | "type": "Pattern", 53 | "elements": [ 54 | { 55 | "type": "TextElement", 56 | "value": "Foo", 57 | "span": { 58 | "type": "Span", 59 | "start": 35, 60 | "end": 38 61 | } 62 | } 63 | ], 64 | "span": { 65 | "type": "Span", 66 | "start": 35, 67 | "end": 38 68 | } 69 | }, 70 | "default": true, 71 | "span": { 72 | "type": "Span", 73 | "start": 26, 74 | "end": 38 75 | } 76 | } 77 | ], 78 | "span": { 79 | "type": "Span", 80 | "start": 10, 81 | "end": 43 82 | } 83 | }, 84 | "span": { 85 | "type": "Span", 86 | "start": 8, 87 | "end": 44 88 | } 89 | } 90 | ], 91 | "span": { 92 | "type": "Span", 93 | "start": 8, 94 | "end": 44 95 | } 96 | }, 97 | "attributes": [], 98 | "comment": null, 99 | "span": { 100 | "type": "Span", 101 | "start": 0, 102 | "end": 44 103 | } 104 | } 105 | ], 106 | "span": { 107 | "type": "Span", 108 | "start": 0, 109 | "end": 45 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_symbol_with_space.ftl: -------------------------------------------------------------------------------- 1 | -err01 = { $sel -> 2 | *[New York] Nowy Jork 3 | } 4 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variant_with_symbol_with_space.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0003", 10 | "arguments": [ 11 | "]" 12 | ], 13 | "message": "Expected token: \"]\"", 14 | "span": { 15 | "type": "Span", 16 | "start": 33, 17 | "end": 33 18 | } 19 | } 20 | ], 21 | "content": "-err01 = { $sel ->\n *[New York] Nowy Jork\n }\n", 22 | "span": { 23 | "type": "Span", 24 | "start": 0, 25 | "end": 55 26 | } 27 | } 28 | ], 29 | "span": { 30 | "type": "Span", 31 | "start": 0, 32 | "end": 55 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variants_with_two_defaults.ftl: -------------------------------------------------------------------------------- 1 | -err01 = { $sel -> 2 | *[one] Foo 3 | *[two] Two 4 | } 5 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/variants_with_two_defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Resource", 3 | "body": [ 4 | { 5 | "type": "Junk", 6 | "annotations": [ 7 | { 8 | "type": "Annotation", 9 | "code": "E0015", 10 | "arguments": [], 11 | "message": "Only one variant can be marked as default (*)", 12 | "span": { 13 | "type": "Span", 14 | "start": 38, 15 | "end": 38 16 | } 17 | } 18 | ], 19 | "content": "-err01 = { $sel ->\n *[one] Foo\n *[two] Two\n }\n", 20 | "span": { 21 | "type": "Span", 22 | "start": 0, 23 | "end": 55 24 | } 25 | } 26 | ], 27 | "span": { 28 | "type": "Span", 29 | "start": 0, 30 | "end": 55 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/whitespace_leading.ftl: -------------------------------------------------------------------------------- 1 | # < whitespace > 2 | key1 = Value 3 | 4 | # ↓ nbsp 5 | key2 =   Value 6 | 7 | key3 = {""} Value 8 | key4 = {" "}Value 9 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/fixtures_structure/whitespace_trailing.ftl: -------------------------------------------------------------------------------- 1 | # < whitespace > 2 | key1 = Value 3 | 4 | # ↓ nbsp 5 | key2 = Value   6 | 7 | key3 = Value {placeable}. 8 | key4 = Value{" "} 9 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/test_ast_json.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from fluent.syntax.ast import from_json 4 | from fluent.syntax.parser import FluentParser 5 | from tests.syntax import dedent_ftl 6 | 7 | 8 | class TestASTJSON(unittest.TestCase): 9 | maxDiff = None 10 | 11 | def setUp(self): 12 | self.parser = FluentParser() 13 | 14 | def test_simple_resource(self): 15 | input = """\ 16 | foo = Foo 17 | """ 18 | 19 | ast1 = self.parser.parse(dedent_ftl(input)) 20 | json1 = ast1.to_json() 21 | ast2 = from_json(json1) 22 | json2 = ast2.to_json() 23 | 24 | self.assertEqual(json1, json2) 25 | 26 | def test_complex_resource(self): 27 | input = """\ 28 | ### A Resource comment 29 | 30 | # A comment about shared-photos 31 | shared-photos = 32 | { $user_name } { $photo_count -> 33 | [0] hasn't added any photos yet 34 | [one] added a new photo 35 | *[other] added { $photo_count } new photos 36 | }. 37 | 38 | 39 | ## A Section comment 40 | 41 | // A Syntax 0.4 comment about liked-comment 42 | liked-comment = 43 | { $user_name } liked your comment on { $user_gender -> 44 | [male] his 45 | [female] her 46 | *[other] their 47 | } post. 48 | """ 49 | 50 | ast1 = self.parser.parse(dedent_ftl(input)) 51 | json1 = ast1.to_json() 52 | ast2 = from_json(json1) 53 | json2 = ast2.to_json() 54 | 55 | self.assertEqual(json1, json2) 56 | 57 | def test_syntax_error(self): 58 | input = """\ 59 | foo = Foo { 60 | """ 61 | 62 | ast1 = self.parser.parse(dedent_ftl(input)) 63 | json1 = ast1.to_json() 64 | ast2 = from_json(json1) 65 | json2 = ast2.to_json() 66 | 67 | self.assertEqual(json1, json2) 68 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/test_literal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from fluent.syntax.parser import FluentParser 5 | 6 | 7 | def parse_literal(input): 8 | parser = FluentParser(with_spans=False) 9 | ast = parser.parse_entry(input) 10 | expr = ast.value.elements[0].expression 11 | return expr.parse() 12 | 13 | 14 | class TestStringLiteralParse(unittest.TestCase): 15 | def test_no_escape_sequences(self): 16 | self.assertDictEqual(parse_literal('x = {"abc"}'), {"value": "abc"}) 17 | 18 | def test_double_quote_backslash(self): 19 | self.assertDictEqual(parse_literal(r'x = {"\""}'), {"value": '"'}) 20 | self.assertDictEqual(parse_literal(r'x = {"\\"}'), {"value": "\\"}) 21 | 22 | def test_unicode_escape(self): 23 | self.assertDictEqual(parse_literal('x = {"\\u0041"}'), {"value": "A"}) 24 | self.assertDictEqual(parse_literal('x = {"\\\\u0041"}'), {"value": "\\u0041"}) 25 | if sys.maxunicode > 0xFFFF: 26 | self.assertDictEqual(parse_literal('x = {"\\U01F602"}'), {"value": "😂"}) 27 | self.assertDictEqual( 28 | parse_literal('x = {"\\\\U01F602"}'), {"value": "\\U01F602"} 29 | ) 30 | 31 | def test_trailing_number(self): 32 | self.assertDictEqual(parse_literal('x = {"\\u004100"}'), {"value": "A00"}) 33 | if sys.maxunicode > 0xFFFF: 34 | self.assertDictEqual( 35 | parse_literal('x = {"\\U01F60200"}'), {"value": "😂00"} 36 | ) 37 | 38 | 39 | class TestNumberLiteralParse(unittest.TestCase): 40 | def test_integers(self): 41 | self.assertDictEqual(parse_literal("x = {0}"), {"value": 0, "precision": 0}) 42 | self.assertDictEqual(parse_literal("x = {1}"), {"value": 1, "precision": 0}) 43 | self.assertDictEqual(parse_literal("x = {-0}"), {"value": 0, "precision": 0}) 44 | self.assertDictEqual(parse_literal("x = {-1}"), {"value": -1, "precision": 0}) 45 | 46 | def test_padded_integers(self): 47 | self.assertDictEqual(parse_literal("x = {00}"), {"value": 0, "precision": 0}) 48 | self.assertDictEqual(parse_literal("x = {01}"), {"value": 1, "precision": 0}) 49 | self.assertDictEqual(parse_literal("x = {-00}"), {"value": 0, "precision": 0}) 50 | self.assertDictEqual(parse_literal("x = {-01}"), {"value": -1, "precision": 0}) 51 | 52 | def test_positive_floats(self): 53 | self.assertDictEqual(parse_literal("x = {0.0}"), {"value": 0, "precision": 1}) 54 | self.assertDictEqual( 55 | parse_literal("x = {0.01}"), {"value": 0.01, "precision": 2} 56 | ) 57 | self.assertDictEqual( 58 | parse_literal("x = {1.03}"), {"value": 1.03, "precision": 2} 59 | ) 60 | self.assertDictEqual(parse_literal("x = {1.000}"), {"value": 1, "precision": 3}) 61 | 62 | def test_negative_floats(self): 63 | self.assertDictEqual(parse_literal("x = {-0.0}"), {"value": 0, "precision": 1}) 64 | self.assertDictEqual( 65 | parse_literal("x = {-0.01}"), {"value": -0.01, "precision": 2} 66 | ) 67 | self.assertDictEqual( 68 | parse_literal("x = {-1.03}"), {"value": -1.03, "precision": 2} 69 | ) 70 | self.assertDictEqual( 71 | parse_literal("x = {-1.000}"), {"value": -1, "precision": 3} 72 | ) 73 | 74 | def test_padded_floats(self): 75 | self.assertDictEqual( 76 | parse_literal("x = {-00.00}"), {"value": 0, "precision": 2} 77 | ) 78 | self.assertDictEqual( 79 | parse_literal("x = {-01.000}"), {"value": -1, "precision": 3} 80 | ) 81 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/test_reference.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import os 4 | import unittest 5 | 6 | from fluent.syntax import parse 7 | 8 | 9 | def read_file(path): 10 | with codecs.open(path, "r", encoding="utf-8") as file: 11 | text = file.read() 12 | return text 13 | 14 | 15 | fixtures = os.path.join(os.path.dirname(__file__), "fixtures_reference") 16 | 17 | 18 | class TestReferenceMeta(type): 19 | def __new__(mcs, name, bases, attrs): 20 | 21 | def remove_untested(obj): 22 | if obj["type"] == "Junk": 23 | obj["annotations"] = [] 24 | if "span" in obj: 25 | del obj["span"] 26 | return obj 27 | 28 | def gen_test(file_name): 29 | def test(self): 30 | ftl_path = os.path.join(fixtures, file_name + ".ftl") 31 | ast_path = os.path.join(fixtures, file_name + ".json") 32 | 33 | source = read_file(ftl_path) 34 | expected = read_file(ast_path) 35 | 36 | ast = parse(source) 37 | self.assertEqual(ast.to_json(remove_untested), json.loads(expected)) 38 | 39 | return test 40 | 41 | for f in os.listdir(fixtures): 42 | file_name, ext = os.path.splitext(f) 43 | 44 | if ext != ".ftl": 45 | continue 46 | 47 | # Skip fixtures which are known to differ between the reference 48 | # parser and the tooling parser. 49 | if file_name in ("leading_dots", "variant_lists"): 50 | continue 51 | 52 | test_name = f"test_{file_name}" 53 | attrs[test_name] = gen_test(file_name) 54 | 55 | return type.__new__(mcs, name, bases, attrs) 56 | 57 | 58 | class TestReference(unittest.TestCase, metaclass=TestReferenceMeta): 59 | maxDiff = None 60 | -------------------------------------------------------------------------------- /fluent.syntax/tests/syntax/test_structure.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import os 4 | import unittest 5 | 6 | from fluent.syntax import parse 7 | 8 | 9 | def read_file(path): 10 | with codecs.open(path, "r", encoding="utf-8") as file: 11 | text = file.read() 12 | return text 13 | 14 | 15 | def without_spans(expected): 16 | """ 17 | Given an expected JSON fragment with span information, recursively replace all of the spans 18 | with None. 19 | """ 20 | if isinstance(expected, dict): 21 | result = {} 22 | for key, value in expected.items(): 23 | if key == "span": 24 | result[key] = None 25 | else: 26 | result[key] = without_spans(value) 27 | 28 | return result 29 | elif isinstance(expected, list): 30 | return [without_spans(item) for item in expected] 31 | else: 32 | # We have been passed something which would not have span information in it 33 | return expected 34 | 35 | 36 | fixtures = os.path.join(os.path.dirname(__file__), "fixtures_structure") 37 | 38 | 39 | class TestStructureMeta(type): 40 | def __new__(mcs, name, bases, attrs): 41 | 42 | def gen_test(file_name, with_spans): 43 | def test(self): 44 | ftl_path = os.path.join(fixtures, file_name + ".ftl") 45 | ast_path = os.path.join(fixtures, file_name + ".json") 46 | 47 | source = read_file(ftl_path) 48 | expected = json.loads(read_file(ast_path)) 49 | 50 | if not with_spans: 51 | expected = without_spans(expected) 52 | 53 | ast = parse(source, with_spans=with_spans) 54 | 55 | self.assertEqual(ast.to_json(), expected) 56 | 57 | return test 58 | 59 | for f in os.listdir(fixtures): 60 | file_name, ext = os.path.splitext(f) 61 | 62 | if ext != ".ftl": 63 | continue 64 | 65 | attrs[f"test_{file_name}_with_spans"] = gen_test(file_name, with_spans=True) 66 | attrs[f"test_{file_name}_without_spans"] = gen_test(file_name, with_spans=False) 67 | 68 | return type.__new__(mcs, name, bases, attrs) 69 | 70 | 71 | class TestStructure(unittest.TestCase, metaclass=TestStructureMeta): 72 | maxDiff = None 73 | -------------------------------------------------------------------------------- /fluent.syntax/tox.ini: -------------------------------------------------------------------------------- 1 | # This config is for local testing. 2 | # It should be correspond to .github/workflows/fluent.syntax.yml 3 | [tox] 4 | envlist = py36, py37, py38, py39, pypy3 5 | skipsdist=True 6 | 7 | [testenv] 8 | setenv = 9 | PYTHONPATH = {toxinidir} 10 | deps = 11 | typing-extensions~=3.7 12 | commands = python -m unittest 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | line_length = 120 3 | skip_glob = ".tox" 4 | 5 | [tool.mypy] 6 | strict = true 7 | -------------------------------------------------------------------------------- /scripts/build-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from datetime import date 5 | 6 | from fluent.docs import build_root, finalize_builddir 7 | from fluent.docs.build import build 8 | 9 | if __name__ == "__main__": 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument( 12 | 'repo_name', 13 | help='URL prefix on the web. Repo name on gh-pages' 14 | ) 15 | default_docs = ['.', 'fluent.syntax', 'fluent.pygments', 'fluent.runtime'] 16 | parser.add_argument( 17 | '--doc', action='append', choices=default_docs, 18 | help='Only build select documentation roots.' 19 | ) 20 | parser.add_argument( 21 | '--dev', action='store_false', dest='show_releases', 22 | help='Only build development versions of projects' 23 | ) 24 | args = parser.parse_args() 25 | if args.doc is None: 26 | args.doc = default_docs[:] 27 | if '.' in args.doc: 28 | build_root(args.repo_name) 29 | args.doc.remove('.') 30 | releases_after = None 31 | if args.show_releases: 32 | # python-fluent had descent docs since May 2020 33 | releases_after = date(2020, 5, 1) 34 | build(args.repo_name, args.doc, releases_after) 35 | finalize_builddir(args.repo_name) 36 | -------------------------------------------------------------------------------- /tools/fluentfmt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import codecs 4 | import sys 5 | 6 | from fluent.syntax import parse, serialize 7 | 8 | sys.path.append("./") 9 | 10 | 11 | def read_file(path): 12 | with codecs.open(path, "r", encoding="utf-8") as file: 13 | text = file.read() 14 | return text 15 | 16 | 17 | def pretty_print(fileType, data): 18 | ast = parse(data) 19 | print(serialize(ast)) 20 | 21 | 22 | if __name__ == "__main__": 23 | file_type = "ftl" 24 | f = read_file(sys.argv[1]) 25 | pretty_print(file_type, f) 26 | -------------------------------------------------------------------------------- /tools/parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import codecs 4 | import json 5 | import sys 6 | 7 | from fluent.syntax import parse 8 | 9 | sys.path.append("./") 10 | 11 | 12 | def read_file(path): 13 | with codecs.open(path, "r", encoding="utf-8") as file: 14 | text = file.read() 15 | return text 16 | 17 | 18 | def print_ast(fileType, data): 19 | ast = parse(data) 20 | print(json.dumps(ast.to_json(), indent=2, ensure_ascii=False)) 21 | 22 | 23 | if __name__ == "__main__": 24 | file_type = "ftl" 25 | f = read_file(sys.argv[1]) 26 | print_ast(file_type, f) 27 | -------------------------------------------------------------------------------- /tools/serialize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import codecs 4 | import json 5 | import sys 6 | 7 | from fluent.syntax import ast, serialize 8 | 9 | sys.path.append("./") 10 | 11 | 12 | def read_json(path): 13 | with codecs.open(path, "r", encoding="utf-8") as file: 14 | return json.load(file) 15 | 16 | 17 | def pretty_print(fileType, data): 18 | resource = ast.from_json(data) 19 | print(serialize(resource)) 20 | 21 | 22 | if __name__ == "__main__": 23 | file_type = "ftl" 24 | f = read_json(sys.argv[1]) 25 | pretty_print(file_type, f) 26 | --------------------------------------------------------------------------------