diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7a7aab..02b507c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,12 +47,23 @@ This will set up your local development environment, installing all development ### Testing (single Python version) -To run the test suite using the Python version of your virtual environment, run: +Run all Rust and Python tests (within your specific virtual environment) using this command: ```sh make test ``` +This will recompile the Rust code (if necessary) before running the tests. + +#### Using pytest directly + +You can also run tests using pytest directly (e.g. via your IDE), but you will need to recompile the Rust code after +any changes, otherwise they won't get picked up. You can recompile by running: + +```sh +maturin develop +``` + ### Testing (all supported Python versions) To test against multiple Python (and package) versions, we need to: diff --git a/README.md b/README.md index 255b85f..c54323e 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ pip install rustfluent ## Usage ```python -import rustfluent as fluent +import rustfluent # First load a bundle -bundle = fluent.Bundle( +bundle = rustfluent.Bundle( "en", [ # Multiple FTL files can be specified. Entries in later @@ -40,13 +40,67 @@ bundle = fluent.Bundle( # Fetch a translation assert bundle.get_translation("hello-world") == "Hello World" -# Fetch a translation that takes a keyword argument -assert bundle.get_translation("hello-user", user="Bob") == "Hello, \u2068Bob\u2069" +# Fetch a translation that includes variables +assert bundle.get_translation("hello-user", variables={"user": "Bob"}) == "Hello, \u2068Bob\u2069" ``` The Unicode characters around "Bob" in the above example are for [Unicode bidirectional handling](https://www.unicode.org/reports/tr9/). +## API reference + +### `Bundle` class + +A set of translations for a specific language. + +```python +import rustfluent + +bundle = rustfluent.Bundle( + language="en-US", + ftl_files=[ + "/path/to/messages.ftl", + "/path/to/more/messages.ftl", + ], +) +``` + +#### Parameters + +| Name | Type | Description | +|-------------|-------------|-------------------------------------------------------------------------------------------------------------------------| +| `language` | `str` | [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) for the language. | +| `ftl_files` | `list[str]` | Full paths to the FTL files containing the translations. Entries in later files overwrite earlier ones. | + +#### Raises + +- `FileNotFoundError` if any of the FTL files could not be found. +- `ValueError` if any of the FTL files contain errors. + +### `Bundle.get_translation` + +``` +>>> bundle.get_translation(identifier="hello-world") +"Hello, world!" +>>> bundle.get_translation(identifier="hello-user", variables={"user": "Bob"}) +"Hello, Bob!" +``` + +#### Parameters + +| Name | Type | Description | +|--------------|----------------------------|------------------------------------------------------------------------------------------------------------| +| `identifier` | `str` | The identifier for the Fluent message. | +| `variables` | `dict[str, str]`, optional | Any [variables](https://projectfluent.org/fluent/guide/variables.html) to be passed to the Fluent message. | + +#### Return value + +`str`: the translated message. + +#### Raises + +- `ValueError` if the message could not be found or has no translation available. + ## Contributing See [Contributing](./CONTRIBUTING.md). diff --git a/makefile b/makefile index 0d69712..5233ee2 100644 --- a/makefile +++ b/makefile @@ -14,6 +14,7 @@ dev: install_python_packages .git/hooks/pre-commit .PHONY:test test: cargo test + maturin develop pytest .PHONY:matrix_test diff --git a/pyproject.toml b/pyproject.toml index d52cee2..dfa5c67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ "bump-my-version", # Workflow + "pip", # Without this, `maturin develop` won't work. "pre-commit", "maturin", ] diff --git a/requirements/development.txt b/requirements/development.txt index 3ee93be..f012603 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -6,8 +6,6 @@ argcomplete==3.5.0 # via nox bracex==2.5 # via wcmatch -build==1.2.2 - # via rustfluent (pyproject.toml) bump-my-version==0.26.0 # via rustfluent (pyproject.toml) cfgv==3.4.0 @@ -42,9 +40,10 @@ nox==2024.4.15 # via rustfluent (pyproject.toml) packaging==24.1 # via - # build # nox # pytest +pip==24.2 + # via rustfluent (pyproject.toml) platformdirs==4.3.2 # via virtualenv pluggy==1.5.0 @@ -63,8 +62,6 @@ pydantic-settings==2.5.2 # via bump-my-version pygments==2.18.0 # via rich -pyproject-hooks==1.1.0 - # via build pytest==8.3.3 # via rustfluent (pyproject.toml) python-dotenv==1.0.1 diff --git a/src/lib.rs b/src/lib.rs index 2c2ae4a..8429b80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,9 +21,9 @@ struct Bundle { #[pymethods] impl Bundle { #[new] - fn new(namespace: &str, ftl_filenames: &'_ Bound<'_, PyList>) -> PyResult { - let langid_en: LanguageIdentifier = namespace.parse().expect("Parsing failed"); - let mut bundle = FluentBundle::new_concurrent(vec![langid_en]); + fn new(language: &str, ftl_filenames: &'_ Bound<'_, PyList>) -> PyResult { + let langid: LanguageIdentifier = language.parse().expect("Parsing failed"); + let mut bundle = FluentBundle::new_concurrent(vec![langid]); for file_path in ftl_filenames.iter() { let path_string = file_path.to_string(); @@ -47,11 +47,11 @@ impl Bundle { Ok(Self { bundle }) } - #[pyo3(signature = (identifier, **kwargs))] + #[pyo3(signature = (identifier, variables=None))] pub fn get_translation( &self, identifier: &str, - kwargs: Option<&Bound<'_, PyDict>>, + variables: Option<&Bound<'_, PyDict>>, ) -> PyResult { let msg = match self.bundle.get_message(identifier) { Some(m) => m, @@ -71,9 +71,9 @@ impl Bundle { let mut args = FluentArgs::new(); - if let Some(kwargs) = kwargs { - for kwarg in kwargs { - args.set(kwarg.0.to_string(), kwarg.1.to_string()); + if let Some(variables) = variables { + for variable in variables { + args.set(variable.0.to_string(), variable.1.to_string()); } } diff --git a/src/rustfluent.pyi b/src/rustfluent.pyi index d6a3ab3..2f3afac 100644 --- a/src/rustfluent.pyi +++ b/src/rustfluent.pyi @@ -1,3 +1,3 @@ class Bundle: - def __init__(self, namespace: str, ftl_filenames: list[str]) -> None: ... - def get_translation(self, identifier: str, **kwargs: str) -> str: ... + def __init__(self, language: str, ftl_filenames: list[str]) -> None: ... + def get_translation(self, identifier: str, variables: dict[str, str] | None = None) -> str: ... diff --git a/tests/rustfluent/data/en.ftl b/tests/data/en.ftl similarity index 100% rename from tests/rustfluent/data/en.ftl rename to tests/data/en.ftl diff --git a/tests/rustfluent/data/en_hello.ftl b/tests/data/en_hello.ftl similarity index 100% rename from tests/rustfluent/data/en_hello.ftl rename to tests/data/en_hello.ftl diff --git a/tests/rustfluent/data/errors.ftl b/tests/data/errors.ftl similarity index 100% rename from tests/rustfluent/data/errors.ftl rename to tests/data/errors.ftl diff --git a/tests/rustfluent/data/fr.ftl b/tests/data/fr.ftl similarity index 100% rename from tests/rustfluent/data/fr.ftl rename to tests/data/fr.ftl diff --git a/tests/rustfluent/__init__.py b/tests/rustfluent/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/rustfluent/test_python_interface.py b/tests/rustfluent/test_python_interface.py deleted file mode 100644 index 70859d6..0000000 --- a/tests/rustfluent/test_python_interface.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python - -import pathlib - -import pytest - -import rustfluent as fluent - - -data_dir = pathlib.Path(__file__).parent.resolve() / "data" - - -def test_en_basic(): - bundle = fluent.Bundle( - "en", - [ - str(data_dir / "en.ftl"), - ], - ) - assert bundle.get_translation("hello-world") == "Hello World" - - -def test_en_with_args(): - bundle = fluent.Bundle( - "en", - [ - str(data_dir / "en.ftl"), - ], - ) - assert bundle.get_translation("hello-user", user="Bob") == "Hello, \u2068Bob\u2069" - - -def test_fr_basic(): - bundle = fluent.Bundle( - "fr", - [ - str(data_dir / "fr.ftl"), - ], - ) - assert bundle.get_translation("hello-world") == "Bonjour le monde!" - - -def test_fr_with_args(): - bundle = fluent.Bundle( - "fr", - [ - str(data_dir / "fr.ftl"), - ], - ) - assert bundle.get_translation("hello-user", user="Bob") == "Bonjour, \u2068Bob\u2069!" - - -def test_new_overwrites_old(): - bundle = fluent.Bundle( - "en", - [ - str(data_dir / "fr.ftl"), - str(data_dir / "en_hello.ftl"), - ], - ) - assert bundle.get_translation("hello-world") == "Hello World" - assert bundle.get_translation("hello-user", user="Bob") == "Bonjour, \u2068Bob\u2069!" - - -def test_id_not_found(): - bundle = fluent.Bundle( - "fr", - [ - str(data_dir / "fr.ftl"), - ], - ) - with pytest.raises(ValueError): - bundle.get_translation("missing", user="Bob") - - -def test_file_not_found(): - with pytest.raises(FileNotFoundError): - fluent.Bundle( - "fr", - [ - str(data_dir / "none.ftl"), - ], - ) - - -def test_file_has_errors(): - with pytest.raises(ValueError): - fluent.Bundle( - "fr", - [ - str(data_dir / "errors.ftl"), - ], - ) diff --git a/tests/rustfluent/test_package.py b/tests/test_package.py similarity index 100% rename from tests/rustfluent/test_package.py rename to tests/test_package.py diff --git a/tests/test_python_interface.py b/tests/test_python_interface.py new file mode 100644 index 0000000..7cc9552 --- /dev/null +++ b/tests/test_python_interface.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +import pathlib + +import pytest + +import rustfluent as fluent + + +data_dir = pathlib.Path(__file__).parent.resolve() / "data" + + +def test_en_basic(): + bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")]) + assert bundle.get_translation("hello-world") == "Hello World" + + +def test_en_basic_with_named_arguments(): + bundle = fluent.Bundle( + language="en", + ftl_filenames=[str(data_dir / "en.ftl")], + ) + assert bundle.get_translation("hello-world") == "Hello World" + + +def test_en_with_args(): + bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")]) + assert ( + bundle.get_translation("hello-user", variables={"user": "Bob"}) == "Hello, \u2068Bob\u2069" + ) + + +def test_fr_basic(): + bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")]) + assert bundle.get_translation("hello-world") == "Bonjour le monde!" + + +def test_fr_with_args(): + bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")]) + assert ( + bundle.get_translation("hello-user", variables={"user": "Bob"}) + == "Bonjour, \u2068Bob\u2069!" + ) + + +def test_new_overwrites_old(): + bundle = fluent.Bundle( + "en", + [str(data_dir / "fr.ftl"), str(data_dir / "en_hello.ftl")], + ) + assert bundle.get_translation("hello-world") == "Hello World" + assert ( + bundle.get_translation("hello-user", variables={"user": "Bob"}) + == "Bonjour, \u2068Bob\u2069!" + ) + + +def test_id_not_found(): + bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")]) + with pytest.raises(ValueError): + bundle.get_translation("missing", variables={"user": "Bob"}) + + +def test_file_not_found(): + with pytest.raises(FileNotFoundError): + fluent.Bundle("fr", [str(data_dir / "none.ftl")]) + + +def test_file_has_errors(): + with pytest.raises(ValueError): + fluent.Bundle("fr", [str(data_dir / "errors.ftl")])