mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-17 22:08:39 +00:00
Python: Added support for run-time translations from .slint
files
Fixes #7956
This commit is contained in:
parent
ca4e337146
commit
2bbe5e8786
12 changed files with 221 additions and 5 deletions
|
@ -43,7 +43,7 @@ accessibility = ["slint-interpreter/accessibility"]
|
|||
|
||||
[dependencies]
|
||||
i-slint-backend-selector = { workspace = true }
|
||||
i-slint-core = { workspace = true }
|
||||
i-slint-core = { workspace = true, features = ["tr"] }
|
||||
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
|
||||
i-slint-compiler = { workspace = true }
|
||||
pyo3 = { version = "0.26", features = ["extension-module", "indexmap", "chrono", "abi3-py311"] }
|
||||
|
@ -53,6 +53,7 @@ spin_on = { workspace = true }
|
|||
css-color-parser2 = { workspace = true }
|
||||
pyo3-stub-gen = { version = "0.9.0", default-features = false }
|
||||
smol = { version = "2.0.0" }
|
||||
tr = { workspace = true }
|
||||
|
||||
[package.metadata.maturin]
|
||||
python-source = "slint"
|
||||
|
|
|
@ -78,6 +78,80 @@ fn invoke_from_event_loop(callable: Py<PyAny>) -> Result<(), errors::PyEventLoop
|
|||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[gen_stub_pyfunction]
|
||||
#[pyfunction]
|
||||
fn init_translations(_py: Python<'_>, translations: Bound<PyAny>) -> PyResult<()> {
|
||||
i_slint_backend_selector::with_global_context(|ctx| {
|
||||
ctx.set_external_translator(if translations.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(PyGettextTranslator(translations.unbind())))
|
||||
});
|
||||
i_slint_core::translations::mark_all_translations_dirty();
|
||||
})
|
||||
.map_err(|e| errors::PyPlatformError(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PyGettextTranslator(Py<PyAny>);
|
||||
|
||||
impl tr::Translator for PyGettextTranslator {
|
||||
fn translate<'a>(
|
||||
&'a self,
|
||||
string: &'a str,
|
||||
context: Option<&'a str>,
|
||||
) -> std::borrow::Cow<'a, str> {
|
||||
Python::attach(|py| {
|
||||
match if let Some(context) = context {
|
||||
self.0.call_method(py, pyo3::intern!(py, "pgettext"), (context, string), None)
|
||||
} else {
|
||||
self.0.call_method(py, pyo3::intern!(py, "gettext"), (string,), None)
|
||||
} {
|
||||
Ok(translation) => Some(translation),
|
||||
Err(err) => {
|
||||
handle_unraisable(py, "calling pgettext/gettext".into(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
.and_then(|maybe_str| maybe_str.extract::<String>(py).ok())
|
||||
.map(std::borrow::Cow::Owned)
|
||||
})
|
||||
.unwrap_or(std::borrow::Cow::Borrowed(string))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn ntranslate<'a>(
|
||||
&'a self,
|
||||
n: u64,
|
||||
singular: &'a str,
|
||||
plural: &'a str,
|
||||
context: Option<&'a str>,
|
||||
) -> std::borrow::Cow<'a, str> {
|
||||
Python::attach(|py| {
|
||||
match if let Some(context) = context {
|
||||
self.0.call_method(
|
||||
py,
|
||||
pyo3::intern!(py, "npgettext"),
|
||||
(context, singular, plural, n),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
self.0.call_method(py, pyo3::intern!(py, "ngettext"), (singular, plural, n), None)
|
||||
} {
|
||||
Ok(translation) => Some(translation),
|
||||
Err(err) => {
|
||||
handle_unraisable(py, "calling npgettext/ngettext".into(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
.and_then(|maybe_str| maybe_str.extract::<String>(py).ok())
|
||||
.map(std::borrow::Cow::Owned)
|
||||
})
|
||||
.unwrap_or(std::borrow::Cow::Borrowed(singular))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pymodule]
|
||||
|
@ -107,6 +181,7 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||
m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(set_xdg_app_id, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(invoke_from_event_loop, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(init_translations, m)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ from .loop import SlintEventLoop
|
|||
from pathlib import Path
|
||||
from collections.abc import Coroutine
|
||||
import asyncio
|
||||
import gettext
|
||||
|
||||
Struct = native.PyStruct
|
||||
|
||||
|
@ -493,6 +494,25 @@ def quit_event_loop() -> None:
|
|||
quit_event.set()
|
||||
|
||||
|
||||
def init_translations(translations: typing.Optional[gettext.GNUTranslations]) -> None:
|
||||
"""Installs the specified translations object to handle translations originating from the Slint code.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import gettext
|
||||
import slint
|
||||
|
||||
translations_dir = os.path.join(os.path.dirname(__file__), "lang")
|
||||
try:
|
||||
translations = gettext.translation("my_app", translations_dir, ["de"])
|
||||
slint.install_translations(translations)
|
||||
except OSError:
|
||||
pass
|
||||
```
|
||||
"""
|
||||
native.init_translations(translations)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CompileError",
|
||||
"Component",
|
||||
|
@ -509,4 +529,5 @@ __all__ = [
|
|||
"callback",
|
||||
"run_event_loop",
|
||||
"quit_event_loop",
|
||||
"init_translations",
|
||||
]
|
||||
|
|
|
@ -12,6 +12,7 @@ import typing
|
|||
from typing import Any, List
|
||||
from collections.abc import Callable, Buffer, Coroutine
|
||||
from enum import Enum, auto
|
||||
import gettext
|
||||
|
||||
class RgbColor:
|
||||
red: int
|
||||
|
@ -146,6 +147,9 @@ def set_xdg_app_id(app_id: str) -> None: ...
|
|||
def invoke_from_event_loop(callable: typing.Callable[[], None]) -> None: ...
|
||||
def run_event_loop() -> None: ...
|
||||
def quit_event_loop() -> None: ...
|
||||
def init_translations(
|
||||
translations: typing.Optional[gettext.GNUTranslations],
|
||||
) -> None: ...
|
||||
|
||||
class PyModelBase:
|
||||
def init_self(self, *args: Any) -> None: ...
|
||||
|
|
|
@ -56,6 +56,8 @@ export component App inherits Window {
|
|||
Rectangle {
|
||||
color: red;
|
||||
}
|
||||
|
||||
in-out property <string> translated: @tr("Yes");
|
||||
}
|
||||
|
||||
component Diag inherits Window { }
|
||||
|
|
39
api/python/slint/tests/test_translations.py
Normal file
39
api/python/slint/tests/test_translations.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
from slint import load_file, init_translations
|
||||
from pathlib import Path
|
||||
import gettext
|
||||
import typing
|
||||
|
||||
|
||||
def base_dir() -> Path:
|
||||
origin = __spec__.origin
|
||||
assert origin is not None
|
||||
base_dir = Path(origin).parent
|
||||
assert base_dir is not None
|
||||
return base_dir
|
||||
|
||||
|
||||
class DummyTranslation:
|
||||
def gettext(self, message: str) -> str:
|
||||
if message == "Yes":
|
||||
return "Ja"
|
||||
return message
|
||||
|
||||
def pgettext(self, context: str, message: str) -> str:
|
||||
return self.gettext(message)
|
||||
|
||||
|
||||
def test_load_file() -> None:
|
||||
module = load_file(base_dir() / "test-load-file.slint")
|
||||
|
||||
testcase = module.App()
|
||||
|
||||
assert testcase.translated == "Yes"
|
||||
init_translations(typing.cast(gettext.GNUTranslations, DummyTranslation()))
|
||||
try:
|
||||
assert testcase.translated == "Ja"
|
||||
finally:
|
||||
init_translations(None)
|
||||
assert testcase.translated == "Yes"
|
Loading…
Add table
Add a link
Reference in a new issue