Skip to content

Commit 86878a9

Browse files
committed
Merge branch 'master' of https://github.com/slint-ui/slint into feature/fontique
Conflicts: Cargo.toml
2 parents 2805946 + 7f4be51 commit 86878a9

File tree

21 files changed

+717
-500
lines changed

21 files changed

+717
-500
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ All notable changes to this project are documented in this file.
3535

3636
### Python
3737

38-
- Python: Added support for asyncio by making the Slint event loop act as asyncio event loop.
38+
- Added support for asyncio by making the Slint event loop act as asyncio event loop.
39+
- Added suport for translations via `slint.init_translations()` accepting a `gettext.GNUTranslation`.
3940

4041
### Tools:
4142

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ glow = { version = "0.16" }
171171
tikv-jemallocator = { version = "0.6" }
172172
wgpu-26 = { package = "wgpu", version = "26", default-features = false }
173173
input = { version = "0.9.0", default-features = false }
174+
tr = { version = "0.1", default-features = false }
174175
fontique = { version = "0.5.0" }
175176

176177
[profile.release]

api/python/slint/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ accessibility = ["slint-interpreter/accessibility"]
4343

4444
[dependencies]
4545
i-slint-backend-selector = { workspace = true }
46-
i-slint-core = { workspace = true }
46+
i-slint-core = { workspace = true, features = ["tr"] }
4747
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
4848
i-slint-compiler = { workspace = true }
4949
pyo3 = { version = "0.26", features = ["extension-module", "indexmap", "chrono", "abi3-py311"] }

api/python/slint/lib.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod errors;
1717
mod models;
1818
mod timer;
1919
mod value;
20+
use i_slint_core::translations::Translator;
2021

2122
fn handle_unraisable(py: Python<'_>, context: String, err: PyErr) {
2223
let exception = err.value(py);
@@ -78,6 +79,83 @@ fn invoke_from_event_loop(callable: Py<PyAny>) -> Result<(), errors::PyEventLoop
7879
.map_err(|e| e.into())
7980
}
8081

82+
#[gen_stub_pyfunction]
83+
#[pyfunction]
84+
fn init_translations(_py: Python<'_>, translations: Bound<PyAny>) -> PyResult<()> {
85+
i_slint_backend_selector::with_global_context(|ctx| {
86+
ctx.set_external_translator(if translations.is_none() {
87+
None
88+
} else {
89+
Some(Box::new(PyGettextTranslator(translations.unbind())))
90+
});
91+
i_slint_core::translations::mark_all_translations_dirty();
92+
})
93+
.map_err(|e| errors::PyPlatformError(e))?;
94+
Ok(())
95+
}
96+
97+
struct PyGettextTranslator(
98+
/// A reference to a `gettext.GNUTranslations` object.
99+
Py<PyAny>,
100+
);
101+
102+
impl Translator for PyGettextTranslator {
103+
fn translate<'a>(
104+
&'a self,
105+
string: &'a str,
106+
context: Option<&'a str>,
107+
) -> std::borrow::Cow<'a, str> {
108+
Python::attach(|py| {
109+
match if let Some(context) = context {
110+
self.0.call_method(py, pyo3::intern!(py, "pgettext"), (context, string), None)
111+
} else {
112+
self.0.call_method(py, pyo3::intern!(py, "gettext"), (string,), None)
113+
} {
114+
Ok(translation) => Some(translation),
115+
Err(err) => {
116+
handle_unraisable(py, "calling pgettext/gettext".into(), err);
117+
None
118+
}
119+
}
120+
.and_then(|maybe_str| maybe_str.extract::<String>(py).ok())
121+
.map(std::borrow::Cow::Owned)
122+
})
123+
.unwrap_or(std::borrow::Cow::Borrowed(string))
124+
.into()
125+
}
126+
127+
fn ntranslate<'a>(
128+
&'a self,
129+
n: u64,
130+
singular: &'a str,
131+
plural: &'a str,
132+
context: Option<&'a str>,
133+
) -> std::borrow::Cow<'a, str> {
134+
Python::attach(|py| {
135+
match if let Some(context) = context {
136+
self.0.call_method(
137+
py,
138+
pyo3::intern!(py, "npgettext"),
139+
(context, singular, plural, n),
140+
None,
141+
)
142+
} else {
143+
self.0.call_method(py, pyo3::intern!(py, "ngettext"), (singular, plural, n), None)
144+
} {
145+
Ok(translation) => Some(translation),
146+
Err(err) => {
147+
handle_unraisable(py, "calling npgettext/ngettext".into(), err);
148+
None
149+
}
150+
}
151+
.and_then(|maybe_str| maybe_str.extract::<String>(py).ok())
152+
.map(std::borrow::Cow::Owned)
153+
})
154+
.unwrap_or(std::borrow::Cow::Borrowed(singular))
155+
.into()
156+
}
157+
}
158+
81159
use pyo3::prelude::*;
82160

83161
#[pymodule]
@@ -107,6 +185,7 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
107185
m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?;
108186
m.add_function(wrap_pyfunction!(set_xdg_app_id, m)?)?;
109187
m.add_function(wrap_pyfunction!(invoke_from_event_loop, m)?)?;
188+
m.add_function(wrap_pyfunction!(init_translations, m)?)?;
110189

111190
Ok(())
112191
}

api/python/slint/slint/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pathlib import Path
2121
from collections.abc import Coroutine
2222
import asyncio
23+
import gettext
2324

2425
Struct = native.PyStruct
2526

@@ -493,6 +494,25 @@ def quit_event_loop() -> None:
493494
quit_event.set()
494495

495496

497+
def init_translations(translations: typing.Optional[gettext.GNUTranslations]) -> None:
498+
"""Installs the specified translations object to handle translations originating from the Slint code.
499+
500+
Example:
501+
```python
502+
import gettext
503+
import slint
504+
505+
translations_dir = os.path.join(os.path.dirname(__file__), "lang")
506+
try:
507+
translations = gettext.translation("my_app", translations_dir, ["de"])
508+
slint.install_translations(translations)
509+
except OSError:
510+
pass
511+
```
512+
"""
513+
native.init_translations(translations)
514+
515+
496516
__all__ = [
497517
"CompileError",
498518
"Component",
@@ -509,4 +529,5 @@ def quit_event_loop() -> None:
509529
"callback",
510530
"run_event_loop",
511531
"quit_event_loop",
532+
"init_translations",
512533
]

api/python/slint/slint/slint.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import typing
1212
from typing import Any, List
1313
from collections.abc import Callable, Buffer, Coroutine
1414
from enum import Enum, auto
15+
import gettext
1516

1617
class RgbColor:
1718
red: int
@@ -146,6 +147,9 @@ def set_xdg_app_id(app_id: str) -> None: ...
146147
def invoke_from_event_loop(callable: typing.Callable[[], None]) -> None: ...
147148
def run_event_loop() -> None: ...
148149
def quit_event_loop() -> None: ...
150+
def init_translations(
151+
translations: typing.Optional[gettext.GNUTranslations],
152+
) -> None: ...
149153

150154
class PyModelBase:
151155
def init_self(self, *args: Any) -> None: ...

api/python/slint/tests/test-load-file.slint

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export component App inherits Window {
5656
Rectangle {
5757
color: red;
5858
}
59+
60+
in-out property <string> translated: @tr("Yes");
5961
}
6062

6163
component Diag inherits Window { }
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright © SixtyFPS GmbH <[email protected]>
2+
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
from slint import load_file, init_translations
5+
from pathlib import Path
6+
import gettext
7+
import typing
8+
9+
10+
def base_dir() -> Path:
11+
origin = __spec__.origin
12+
assert origin is not None
13+
base_dir = Path(origin).parent
14+
assert base_dir is not None
15+
return base_dir
16+
17+
18+
class DummyTranslation:
19+
def gettext(self, message: str) -> str:
20+
if message == "Yes":
21+
return "Ja"
22+
return message
23+
24+
def pgettext(self, context: str, message: str) -> str:
25+
return self.gettext(message)
26+
27+
28+
def test_load_file() -> None:
29+
module = load_file(base_dir() / "test-load-file.slint")
30+
31+
testcase = module.App()
32+
33+
assert testcase.translated == "Yes"
34+
init_translations(typing.cast(gettext.GNUTranslations, DummyTranslation()))
35+
try:
36+
assert testcase.translated == "Ja"
37+
finally:
38+
init_translations(None)
39+
assert testcase.translated == "Yes"

api/rs/build/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ It is meant to allow you to compile the `.slint` files from your `build.rs` scri
77
88
The main entry point of this crate is the [`compile()`] function
99
10+
The generated code must be included in your crate by using the `slint::include_modules!()` macro.
11+
1012
## Example
1113
1214
In your Cargo.toml:
@@ -425,6 +427,8 @@ fn formatter_test() {
425427
/// about how to use the generated code.
426428
///
427429
/// This function can only be called within a build script run by cargo.
430+
///
431+
/// See also [`compile_with_config()`] if you want to specify a configuration.
428432
pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
429433
compile_with_config(path, CompilerConfiguration::default())
430434
}

demos/printerdemo/python/main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import os
88
import copy
99
import sys
10+
import gettext
11+
import typing
1012

1113
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
1214

@@ -66,6 +68,23 @@ def update_jobs(self):
6668
top_item = copy.copy(self.printer_queue[0])
6769
self.printer_queue[0] = top_item
6870

71+
@slint.callback(global_name="PrinterSettings")
72+
def change_language(self, language: int) -> None:
73+
def load_translation(language: str) -> typing.Optional[gettext.GNUTranslations]:
74+
translations_dir = os.path.join(os.path.dirname(__file__), "..", "lang")
75+
try:
76+
return gettext.translation("printerdemo", translations_dir, [language])
77+
except Exception:
78+
return None
79+
80+
if language == 0:
81+
translations = load_translation("en")
82+
elif language == 1:
83+
translations = load_translation("fr")
84+
else:
85+
return
86+
slint.init_translations(translations)
87+
6988

7089
main_window = MainWindow()
7190
main_window.run()

0 commit comments

Comments
 (0)