Skip to content

Commit ff8fe27

Browse files
committed
Revert Simplify Bundle to accept single file instead of list
This reverts commit 8044f07.
1 parent 5f8c09f commit ff8fe27

5 files changed

Lines changed: 83 additions & 58 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10-
- **Breaking:** Simplify `Bundle` constructor to accept a single file path instead of a list. Change `ftl_filenames` parameter to `ftl_filename`.
1110
- Add support for Fluent message attributes via dot notation (e.g., `bundle.get_translation("message.attribute")`).
1211

1312
## [0.1.0a8] - 2025-10-01

‎README.md‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ pip install rustfluent
2929
import rustfluent
3030

3131
# First load a bundle
32-
bundle = rustfluent.Bundle("en", "en.ftl")
32+
bundle = rustfluent.Bundle(
33+
"en",
34+
[
35+
# Multiple FTL files can be specified. Entries in later
36+
# files overwrite earlier ones.
37+
"en.ftl",
38+
],
39+
)
3340

3441
# Fetch a translation
3542
assert bundle.get_translation("hello-world") == "Hello World"
@@ -54,22 +61,25 @@ import rustfluent
5461

5562
bundle = rustfluent.Bundle(
5663
language="en-US",
57-
ftl_filename="/path/to/messages.ftl", # Also accepts pathlib.Path
64+
ftl_files=[
65+
"/path/to/messages.ftl",
66+
pathlib.Path("/path/to/more/messages.ftl"),
67+
],
5868
)
5969
```
6070

6171
#### Parameters
6272

63-
| Name | Type | Description |
64-
|----------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
65-
| `language` | `str` | [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) for the language. |
66-
| `ftl_filename` | `str \| pathlib.Path` | Full path to the FTL file containing the translations. |
67-
| `strict` | `bool`, optional | In strict mode, a `ParserError` will be raised if there are any errors in the file. In non-strict mode, invalid Fluent messages will be excluded from the Bundle. |
73+
| Name | Type | Description |
74+
|-------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
75+
| `language` | `str` | [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) for the language. |
76+
| `ftl_files` | `list[str | pathlib.Path]` | Full paths to the FTL files containing the translations. Entries in later files overwrite earlier ones. |
77+
| `strict` | `bool`, optional | In strict mode, a `ParserError` will be raised if there are any errors in the file. In non-strict mode, invalid Fluent messages will be excluded from the Bundle. |
6878

6979
#### Raises
7080

71-
- `FileNotFoundError` if the FTL file could not be found.
72-
- `rustfluent.ParserError` if the FTL file contains errors (strict mode only).
81+
- `FileNotFoundError` if any of the FTL files could not be found.
82+
- `rustfluent.ParserError` if any of the FTL files contain errors (strict mode only).
7383

7484
### `Bundle.get_translation`
7585

‎src/lib.rs‎

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ mod rustfluent {
2929
#[pymethods]
3030
impl Bundle {
3131
#[new]
32-
#[pyo3(signature = (language, ftl_filename, strict=false))]
33-
fn new(language: &str, ftl_filename: PathBuf, strict: bool) -> PyResult<Self> {
32+
#[pyo3(signature = (language, ftl_filenames, strict=false))]
33+
fn new(language: &str, ftl_filenames: Vec<PathBuf>, strict: bool) -> PyResult<Self> {
3434
let langid: LanguageIdentifier = match language.parse() {
3535
Ok(langid) => langid,
3636
Err(_) => {
@@ -41,27 +41,29 @@ mod rustfluent {
4141
};
4242
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
4343

44-
let contents = fs::read_to_string(&ftl_filename)
45-
.map_err(|_| PyFileNotFoundError::new_err(ftl_filename.clone()))?;
44+
for file_path in ftl_filenames.iter() {
45+
let contents = fs::read_to_string(file_path)
46+
.map_err(|_| PyFileNotFoundError::new_err(file_path.clone()))?;
4647

47-
let resource = match FluentResource::try_new(contents) {
48-
Ok(resource) => resource,
49-
Err((resource, errors)) if strict => {
50-
let mut labels = Vec::with_capacity(errors.len());
51-
for error in errors {
52-
labels.push(LabeledSpan::at(error.pos, format!("{}", error.kind)))
48+
let resource = match FluentResource::try_new(contents) {
49+
Ok(resource) => resource,
50+
Err((resource, errors)) if strict => {
51+
let mut labels = Vec::with_capacity(errors.len());
52+
for error in errors {
53+
labels.push(LabeledSpan::at(error.pos, format!("{}", error.kind)))
54+
}
55+
let error = miette!(
56+
labels = labels,
57+
"Error when parsing {}",
58+
file_path.to_string_lossy()
59+
)
60+
.with_source_code(resource.source().to_string());
61+
return Err(ParserError::new_err(format!("{error:?}")));
5362
}
54-
let error = miette!(
55-
labels = labels,
56-
"Error when parsing {}",
57-
ftl_filename.to_string_lossy()
58-
)
59-
.with_source_code(resource.source().to_string());
60-
return Err(ParserError::new_err(format!("{error:?}")));
61-
}
62-
Err((resource, _errors)) => resource,
63-
};
64-
bundle.add_resource_overriding(resource);
63+
Err((resource, _errors)) => resource,
64+
};
65+
bundle.add_resource_overriding(resource);
66+
}
6567

6668
Ok(Self { bundle })
6769
}

‎src/rustfluent.pyi‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ from pathlib import Path
44
Variable = str | int | date
55

66
class Bundle:
7-
def __init__(self, language: str, ftl_filename: str | Path, strict: bool = False) -> None: ...
7+
def __init__(
8+
self, language: str, ftl_filenames: list[str | Path], strict: bool = False
9+
) -> None: ...
810
def get_translation(
911
self,
1012
identifier: str,

‎tests/test_python_interface.py‎

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,33 @@
1515

1616

1717
def test_en_basic():
18-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
18+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
1919
assert bundle.get_translation("hello-world") == "Hello World"
2020

2121

2222
def test_en_basic_str_path():
23-
bundle = fluent.Bundle("en", str(data_dir / "en.ftl"))
23+
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])
2424
assert bundle.get_translation("hello-world") == "Hello World"
2525

2626

2727
def test_en_basic_with_named_arguments():
2828
bundle = fluent.Bundle(
2929
language="en",
30-
ftl_filename=data_dir / "en.ftl",
30+
ftl_filenames=[data_dir / "en.ftl"],
3131
)
3232
assert bundle.get_translation("hello-world") == "Hello World"
3333

3434

3535
def test_en_with_variables():
36-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
36+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
3737
assert (
3838
bundle.get_translation("hello-user", variables={"user": "Bob"})
3939
== f"Hello, {BIDI_OPEN}Bob{BIDI_CLOSE}"
4040
)
4141

4242

4343
def test_en_with_variables_use_isolating_off():
44-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
44+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
4545
assert (
4646
bundle.get_translation(
4747
"hello-user",
@@ -66,7 +66,7 @@ def test_en_with_variables_use_isolating_off():
6666
),
6767
)
6868
def test_variables_of_different_types(description, identifier, variables, expected):
69-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
69+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
7070

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

@@ -75,7 +75,7 @@ def test_variables_of_different_types(description, identifier, variables, expect
7575

7676
def test_invalid_language():
7777
with pytest.raises(ValueError) as exc_info:
78-
fluent.Bundle("$", "")
78+
fluent.Bundle("$", [])
7979

8080
assert str(exc_info.value) == "Invalid language: '$'"
8181

@@ -89,7 +89,7 @@ def test_invalid_language():
8989
),
9090
)
9191
def test_invalid_variable_keys_raise_type_error(key):
92-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
92+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
9393

9494
with pytest.raises(TypeError, match="Variable key not a str, got"):
9595
bundle.get_translation("hello-user", variables={key: "Bob"})
@@ -104,20 +104,20 @@ def test_invalid_variable_keys_raise_type_error(key):
104104
),
105105
)
106106
def test_invalid_variable_values_use_key_instead(value):
107-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
107+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
108108

109109
result = bundle.get_translation("hello-user", variables={"user": value})
110110

111111
assert result == f"Hello, {BIDI_OPEN}user{BIDI_CLOSE}"
112112

113113

114114
def test_fr_basic():
115-
bundle = fluent.Bundle("fr", data_dir / "fr.ftl")
115+
bundle = fluent.Bundle("fr", [data_dir / "fr.ftl"])
116116
assert bundle.get_translation("hello-world") == "Bonjour le monde!"
117117

118118

119119
def test_fr_with_args():
120-
bundle = fluent.Bundle("fr", data_dir / "fr.ftl")
120+
bundle = fluent.Bundle("fr", [data_dir / "fr.ftl"])
121121
assert (
122122
bundle.get_translation("hello-user", variables={"user": "Bob"})
123123
== f"Bonjour, {BIDI_OPEN}Bob{BIDI_CLOSE}!"
@@ -135,22 +135,34 @@ def test_fr_with_args():
135135
),
136136
)
137137
def test_selector(number, expected):
138-
bundle = fluent.Bundle("en", data_dir / "en.ftl")
138+
bundle = fluent.Bundle("en", [data_dir / "en.ftl"])
139139

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

142142
assert result == expected
143143

144144

145+
def test_new_overwrites_old():
146+
bundle = fluent.Bundle(
147+
"en",
148+
[data_dir / "fr.ftl", data_dir / "en_hello.ftl"],
149+
)
150+
assert bundle.get_translation("hello-world") == "Hello World"
151+
assert (
152+
bundle.get_translation("hello-user", variables={"user": "Bob"})
153+
== f"Bonjour, {BIDI_OPEN}Bob{BIDI_CLOSE}!"
154+
)
155+
156+
145157
def test_id_not_found():
146-
bundle = fluent.Bundle("fr", data_dir / "fr.ftl")
158+
bundle = fluent.Bundle("fr", [data_dir / "fr.ftl"])
147159
with pytest.raises(ValueError):
148160
bundle.get_translation("missing", variables={"user": "Bob"})
149161

150162

151163
def test_file_not_found():
152164
with pytest.raises(FileNotFoundError):
153-
fluent.Bundle("fr", data_dir / "none.ftl")
165+
fluent.Bundle("fr", [data_dir / "none.ftl"])
154166

155167

156168
@pytest.mark.parametrize("pass_strict_argument_explicitly", (True, False))
@@ -159,7 +171,7 @@ def test_parses_other_parts_of_file_that_contains_errors_in_non_strict_mode(
159171
):
160172
kwargs = dict(strict=False) if pass_strict_argument_explicitly else {}
161173

162-
bundle = fluent.Bundle("fr", data_dir / "errors.ftl", **kwargs)
174+
bundle = fluent.Bundle("fr", [data_dir / "errors.ftl"], **kwargs)
163175
translation = bundle.get_translation("valid-message")
164176

165177
assert translation == "I'm valid."
@@ -169,7 +181,7 @@ def test_raises_parser_error_on_file_that_contains_errors_in_strict_mode():
169181
filename = data_dir / "errors.ftl"
170182

171183
with pytest.raises(fluent.ParserError) as exc_info:
172-
fluent.Bundle("fr", filename, strict=True)
184+
fluent.Bundle("fr", [filename], strict=True)
173185

174186
message = str(exc_info.value)
175187

@@ -201,31 +213,31 @@ def test_parser_error_str():
201213

202214

203215
def test_basic_attribute_access():
204-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
216+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
205217
assert bundle.get_translation("welcome-message.title") == "Welcome to our site"
206218

207219

208220
def test_regular_message_still_works_with_attributes():
209221
"""Test that accessing the main message value still works when it has attributes."""
210-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
222+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
211223
assert bundle.get_translation("welcome-message") == "Welcome!"
212224

213225

214226
def test_multiple_attributes_on_same_message():
215-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
227+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
216228
assert bundle.get_translation("login-input.placeholder") == "email@example.com"
217229
assert bundle.get_translation("login-input.aria-label") == "Login input value"
218230
assert bundle.get_translation("login-input.title") == "Type your login email"
219231

220232

221233
def test_attribute_with_variables():
222-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
234+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
223235
result = bundle.get_translation("greeting.formal", variables={"name": "Alice"})
224236
assert result == f"Hello, {BIDI_OPEN}Alice{BIDI_CLOSE}"
225237

226238

227239
def test_attribute_with_variables_use_isolating_off():
228-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
240+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
229241
result = bundle.get_translation(
230242
"greeting.informal",
231243
variables={"name": "Bob"},
@@ -235,27 +247,27 @@ def test_attribute_with_variables_use_isolating_off():
235247

236248

237249
def test_attribute_on_message_without_main_value():
238-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
250+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
239251
assert bundle.get_translation("form-button.submit") == "Submit Form"
240252
assert bundle.get_translation("form-button.cancel") == "Cancel"
241253
assert bundle.get_translation("form-button.reset") == "Reset Form"
242254

243255

244256
def test_message_without_value_raises_error():
245257
"""Test that accessing a message without a value (only attributes) raises an error."""
246-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
258+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
247259
with pytest.raises(ValueError, match="form-button - Message has no value"):
248260
bundle.get_translation("form-button")
249261

250262

251263
def test_missing_message_with_attribute_syntax_raises_error():
252-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
264+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
253265
with pytest.raises(ValueError, match="nonexistent not found"):
254266
bundle.get_translation("nonexistent.title")
255267

256268

257269
def test_missing_attribute_raises_error():
258-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
270+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
259271
with pytest.raises(
260272
ValueError,
261273
match="welcome-message.nonexistent - Attribute 'nonexistent' not found on message 'welcome-message'",
@@ -274,5 +286,5 @@ def test_missing_attribute_raises_error():
274286
),
275287
)
276288
def test_attribute_and_message_access_parameterized(identifier, expected):
277-
bundle = fluent.Bundle("en", data_dir / "attributes.ftl")
289+
bundle = fluent.Bundle("en", [data_dir / "attributes.ftl"])
278290
assert bundle.get_translation(identifier) == expected

0 commit comments

Comments
 (0)