Skip to content

Commit

Permalink
Merge pull request #9 from kraken-tech/support-passed-objects
Browse files Browse the repository at this point in the history
Support passed integers
  • Loading branch information
seddonym authored Sep 25, 2024
2 parents 4e6b3a1 + ec79565 commit e0142fe
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

- Limited Fluent variable types to strings and integers.
- Fixed bug where integers could not be used as selectors.

## [0.1.0a2] - 2024-09-23

- Changed error handling during `Bundle` instantiation. Now message errors will be ignored by default, overrideable
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ bundle = rustfluent.Bundle(

#### 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. |
| Name | Type | Description |
|--------------|------------------------------|----------------------------------------|
| `identifier` | `str` | The identifier for the Fluent message. |
| `variables` | `dict[str, str | int ]`, optional | Any [variables](https://projectfluent.org/fluent/guide/variables.html) to be passed to the Fluent message. |

#### Return value

Expand All @@ -101,6 +101,8 @@ bundle = rustfluent.Bundle(
#### Raises

- `ValueError` if the message could not be found or has no translation available.
- `TypeError` if a passed variable is not an instance of `str` or `int`, or if the `int` overflows a signed long
integer (i.e. it's not in the range -2,147,483,648 to 2,147,483,647).

## Contributing

Expand Down
28 changes: 23 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use fluent::FluentArgs;
use fluent_bundle::concurrent::FluentBundle;
use fluent_bundle::FluentResource;
use pyo3::exceptions::{PyFileNotFoundError, PyValueError};
use pyo3::exceptions::{PyFileNotFoundError, PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use pyo3::types::{PyDict, PyInt, PyList, PyString};
use std::fs;
use unic_langid::LanguageIdentifier;

Expand Down Expand Up @@ -85,13 +85,31 @@ impl Bundle {

if let Some(variables) = variables {
for variable in variables {
args.set(variable.0.to_string(), variable.1.to_string());
let key: String = variable.0.to_string();
let python_value = variable.1;
if python_value.is_instance_of::<PyString>() {
args.set(key, python_value.to_string());
} else if python_value.is_instance_of::<PyInt>() {
let int_value: i32 = match python_value.extract() {
Ok(value) => value,
_ => {
return Err(PyTypeError::new_err(format!(
"Integer variable was too large: {}.",
python_value
)));
}
};
args.set(key, int_value);
} else {
return Err(PyTypeError::new_err(
"Expected a string, integer or float.".to_string(),
));
}
}
}

let value = self
.bundle
.format_pattern(&pattern, Some(&args), &mut errors);
.format_pattern(pattern, Some(&args), &mut errors);
Ok(value.to_string())
}
}
6 changes: 5 additions & 1 deletion src/rustfluent.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Variable = str | int

class Bundle:
def __init__(self, language: str, ftl_filenames: list[str], strict: bool = False) -> None: ...
def get_translation(self, identifier: str, variables: dict[str, str] | None = None) -> str: ...
def get_translation(
self, identifier: str, variables: dict[str, Variable] | None = None
) -> str: ...
7 changes: 7 additions & 0 deletions tests/data/en.ftl
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
hello-world = Hello World
hello-user = Hello, { $user }
apples = { $numberOfApples } apples
with-selector = { $number ->
[1] One
*[other] Something else
}
64 changes: 61 additions & 3 deletions tests/test_python_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

data_dir = pathlib.Path(__file__).parent.resolve() / "data"

# Bidirectional markers.
# See https://unicode.org/reports/tr9/#Directional_Formatting_Characters
BIDI_OPEN, BIDI_CLOSE = "\u2068", "\u2069"


def test_en_basic():
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])
Expand All @@ -26,10 +30,46 @@ def test_en_basic_with_named_arguments():
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"
bundle.get_translation("hello-user", variables={"user": "Bob"})
== f"Hello, {BIDI_OPEN}Bob{BIDI_CLOSE}"
)


@pytest.mark.parametrize(
"description, identifier, variables, expected",
(
("String", "hello-user", {"user": "Bob"}, f"Hello, {BIDI_OPEN}Bob{BIDI_CLOSE}"),
("Integer", "apples", {"numberOfApples": 10}, f"{BIDI_OPEN}10{BIDI_CLOSE} apples"),
),
)
def test_variables_of_different_types(description, identifier, variables, expected):
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])

result = bundle.get_translation(identifier, variables=variables)

assert result == expected


def test_large_python_integers_fail_sensibly():
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])

with pytest.raises(
TypeError, match=re.escape("Integer variable was too large: 1000000000000.")
):
bundle.get_translation(
"hello-user",
variables={"user": 1_000_000_000_000},
)


@pytest.mark.parametrize("user", (object(), 34.3))
def test_invalid_type_raises_type_error(user):
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])

with pytest.raises(TypeError):
bundle.get_translation("hello-user", variables={"user": user})


def test_fr_basic():
bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")])
assert bundle.get_translation("hello-world") == "Bonjour le monde!"
Expand All @@ -39,10 +79,28 @@ 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!"
== f"Bonjour, {BIDI_OPEN}Bob{BIDI_CLOSE}!"
)


@pytest.mark.parametrize(
"number, expected",
(
(1, "One"),
(2, "Something else"),
# Note that for selection to work, the variable must be an integer.
# So "1" is not equivalent to 1.
("1", "Something else"),
),
)
def test_selector(number, expected):
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])

result = bundle.get_translation("with-selector", variables={"number": number})

assert result == expected


def test_new_overwrites_old():
bundle = fluent.Bundle(
"en",
Expand All @@ -51,7 +109,7 @@ def test_new_overwrites_old():
assert bundle.get_translation("hello-world") == "Hello World"
assert (
bundle.get_translation("hello-user", variables={"user": "Bob"})
== "Bonjour, \u2068Bob\u2069!"
== f"Bonjour, {BIDI_OPEN}Bob{BIDI_CLOSE}!"
)


Expand Down

0 comments on commit e0142fe

Please sign in to comment.