mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
translations no longer require an open collection
This commit is contained in:
parent
4430c67069
commit
0e931808c9
7 changed files with 141 additions and 17 deletions
|
@ -14,6 +14,11 @@ message BackendInit {
|
||||||
string locale_folder_path = 5;
|
string locale_folder_path = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message I18nBackendInit {
|
||||||
|
repeated string preferred_langs = 4;
|
||||||
|
string locale_folder_path = 5;
|
||||||
|
}
|
||||||
|
|
||||||
// 1-15 reserved for future use; 2047 for errors
|
// 1-15 reserved for future use; 2047 for errors
|
||||||
|
|
||||||
message BackendInput {
|
message BackendInput {
|
||||||
|
|
|
@ -3,11 +3,15 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
# Please leave the coding line in this file to prevent xgettext complaining.
|
# Please leave the coding line in this file to prevent xgettext complaining.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import gettext
|
import gettext
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import anki
|
||||||
|
|
||||||
langs = sorted(
|
langs = sorted(
|
||||||
[
|
[
|
||||||
("Afrikaans", "af_ZA"),
|
("Afrikaans", "af_ZA"),
|
||||||
|
@ -134,11 +138,14 @@ def lang_to_disk_lang(lang: str) -> str:
|
||||||
# the currently set interface language
|
# the currently set interface language
|
||||||
currentLang = "en"
|
currentLang = "en"
|
||||||
|
|
||||||
# the current translation catalog
|
# the current gettext translation catalog
|
||||||
current_catalog: Optional[
|
current_catalog: Optional[
|
||||||
Union[gettext.NullTranslations, gettext.GNUTranslations]
|
Union[gettext.NullTranslations, gettext.GNUTranslations]
|
||||||
] = None
|
] = None
|
||||||
|
|
||||||
|
# the current Fluent translation instance
|
||||||
|
current_i18n: Optional[anki.rsbackend.I18nBackend]
|
||||||
|
|
||||||
# path to locale folder
|
# path to locale folder
|
||||||
locale_folder = ""
|
locale_folder = ""
|
||||||
|
|
||||||
|
@ -159,13 +166,17 @@ def ngettext(single: str, plural: str, n: int) -> str:
|
||||||
|
|
||||||
|
|
||||||
def set_lang(lang: str, locale_dir: str) -> None:
|
def set_lang(lang: str, locale_dir: str) -> None:
|
||||||
global currentLang, current_catalog, locale_folder
|
global currentLang, current_catalog, current_i18n, locale_folder
|
||||||
gettext_dir = os.path.join(locale_dir, "gettext")
|
gettext_dir = os.path.join(locale_dir, "gettext")
|
||||||
|
ftl_dir = os.path.join(locale_dir, "fluent")
|
||||||
|
|
||||||
currentLang = lang
|
currentLang = lang
|
||||||
current_catalog = gettext.translation(
|
current_catalog = gettext.translation(
|
||||||
"anki", gettext_dir, languages=[lang], fallback=True
|
"anki", gettext_dir, languages=[lang], fallback=True
|
||||||
)
|
)
|
||||||
|
current_i18n = anki.rsbackend.I18nBackend(
|
||||||
|
preferred_langs=[lang], ftl_folder=ftl_dir
|
||||||
|
)
|
||||||
locale_folder = locale_dir
|
locale_folder = locale_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -329,15 +329,8 @@ class RustBackend:
|
||||||
)
|
)
|
||||||
|
|
||||||
def translate(self, key: FString, **kwargs: Union[str, int, float]):
|
def translate(self, key: FString, **kwargs: Union[str, int, float]):
|
||||||
args = {}
|
|
||||||
for (k, v) in kwargs.items():
|
|
||||||
if isinstance(v, str):
|
|
||||||
args[k] = pb.TranslateArgValue(str=v)
|
|
||||||
else:
|
|
||||||
args[k] = pb.TranslateArgValue(number=v)
|
|
||||||
|
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(translate_string=pb.TranslateStringIn(key=key, args=args))
|
pb.BackendInput(translate_string=translate_string_in(key, **kwargs))
|
||||||
).translate_string
|
).translate_string
|
||||||
|
|
||||||
def format_time_span(
|
def format_time_span(
|
||||||
|
@ -366,3 +359,28 @@ class RustBackend:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).congrats_learn_msg
|
).congrats_learn_msg
|
||||||
|
|
||||||
|
|
||||||
|
def translate_string_in(
|
||||||
|
key: FString, **kwargs: Union[str, int, float]
|
||||||
|
) -> pb.TranslateStringIn:
|
||||||
|
args = {}
|
||||||
|
for (k, v) in kwargs.items():
|
||||||
|
if isinstance(v, str):
|
||||||
|
args[k] = pb.TranslateArgValue(str=v)
|
||||||
|
else:
|
||||||
|
args[k] = pb.TranslateArgValue(number=v)
|
||||||
|
return pb.TranslateStringIn(key=key, args=args)
|
||||||
|
|
||||||
|
|
||||||
|
class I18nBackend:
|
||||||
|
def __init__(self, preferred_langs: List[str], ftl_folder: str) -> None:
|
||||||
|
init_msg = pb.I18nBackendInit(
|
||||||
|
locale_folder_path=ftl_folder, preferred_langs=preferred_langs
|
||||||
|
)
|
||||||
|
self._backend = ankirspy.open_i18n(init_msg.SerializeToString())
|
||||||
|
|
||||||
|
def translate(self, key: FString, **kwargs: Union[str, int, float]):
|
||||||
|
return self._backend.translate(
|
||||||
|
translate_string_in(key, **kwargs).SerializeToString()
|
||||||
|
)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.rsbackend import FString
|
from anki.rsbackend import FString
|
||||||
|
@ -32,12 +33,8 @@ def locale_dir() -> str:
|
||||||
|
|
||||||
|
|
||||||
def tr(key: FString, **kwargs: Union[str, int, float]) -> str:
|
def tr(key: FString, **kwargs: Union[str, int, float]) -> str:
|
||||||
"""Shortcut to access translations from the backend.
|
"Shortcut to access Fluent translations."
|
||||||
(Currently) requires an open collection."""
|
return anki.lang.current_i18n.translate(key, *kwargs)
|
||||||
if aqt.mw.col:
|
|
||||||
return aqt.mw.col.backend.translate(key, **kwargs)
|
|
||||||
else:
|
|
||||||
return repr(key)
|
|
||||||
|
|
||||||
|
|
||||||
def openHelp(section):
|
def openHelp(section):
|
||||||
|
|
13
qt/tests/test_i18n.py
Normal file
13
qt/tests/test_i18n.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import anki.lang
|
||||||
|
from anki.rsbackend import FString
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_collection_i18n():
|
||||||
|
anki.lang.set_lang("zz", "")
|
||||||
|
tr2 = anki.lang.current_i18n.translate
|
||||||
|
no_uni = anki.lang.without_unicode_isolation
|
||||||
|
assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 reviews"
|
||||||
|
|
||||||
|
anki.lang.set_lang("ja", "")
|
||||||
|
tr2 = anki.lang.current_i18n.translate
|
||||||
|
assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 枚の復習カード"
|
|
@ -493,3 +493,50 @@ fn media_sync_progress(p: &MediaSyncProgress, i18n: &I18n) -> pb::MediaSyncProgr
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standalone I18n backend
|
||||||
|
/// This is a hack to allow translating strings in the GUI
|
||||||
|
/// when a collection is not open, and in the future it should
|
||||||
|
/// either be shared with or merged into the backend object.
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub struct I18nBackend {
|
||||||
|
i18n: I18n,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_i18n_backend(init_msg: &[u8]) -> Result<I18nBackend> {
|
||||||
|
let input: pb::I18nBackendInit = match pb::I18nBackendInit::decode(init_msg) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(_) => return Err(AnkiError::invalid_input("couldn't decode init msg")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let i18n = I18n::new(&input.preferred_langs, input.locale_folder_path);
|
||||||
|
|
||||||
|
Ok(I18nBackend { i18n })
|
||||||
|
}
|
||||||
|
|
||||||
|
impl I18nBackend {
|
||||||
|
pub fn translate(&self, req: &[u8]) -> String {
|
||||||
|
let req = match pb::TranslateStringIn::decode(req) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(_e) => return "decoding error".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.translate_string(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate_string(&self, input: pb::TranslateStringIn) -> String {
|
||||||
|
let key = match pb::FluentString::from_i32(input.key) {
|
||||||
|
Some(key) => key,
|
||||||
|
None => return "invalid key".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = input
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.i18n.trn(key, map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use anki::backend::{init_backend, Backend as RustBackend};
|
use anki::backend::{
|
||||||
|
init_backend, init_i18n_backend, Backend as RustBackend, I18nBackend as RustI18nBackend,
|
||||||
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::types::PyBytes;
|
use pyo3::types::PyBytes;
|
||||||
use pyo3::{exceptions, wrap_pyfunction};
|
use pyo3::{exceptions, wrap_pyfunction};
|
||||||
|
|
||||||
|
// Regular backend
|
||||||
|
//////////////////////////////////
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
struct Backend {
|
struct Backend {
|
||||||
backend: RustBackend,
|
backend: RustBackend,
|
||||||
|
@ -68,11 +73,39 @@ impl Backend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18n backend
|
||||||
|
//////////////////////////////////
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
struct I18nBackend {
|
||||||
|
backend: RustI18nBackend,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn open_i18n(init_msg: &PyBytes) -> PyResult<I18nBackend> {
|
||||||
|
match init_i18n_backend(init_msg.as_bytes()) {
|
||||||
|
Ok(backend) => Ok(I18nBackend { backend }),
|
||||||
|
Err(e) => Err(exceptions::Exception::py_err(format!("{:?}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl I18nBackend {
|
||||||
|
fn translate(&self, input: &PyBytes) -> String {
|
||||||
|
let in_bytes = input.as_bytes();
|
||||||
|
self.backend.translate(in_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module definition
|
||||||
|
//////////////////////////////////
|
||||||
|
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn ankirspy(_py: Python, m: &PyModule) -> PyResult<()> {
|
fn ankirspy(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
m.add_class::<Backend>()?;
|
m.add_class::<Backend>()?;
|
||||||
m.add_wrapped(wrap_pyfunction!(buildhash)).unwrap();
|
m.add_wrapped(wrap_pyfunction!(buildhash)).unwrap();
|
||||||
m.add_wrapped(wrap_pyfunction!(open_backend)).unwrap();
|
m.add_wrapped(wrap_pyfunction!(open_backend)).unwrap();
|
||||||
|
m.add_wrapped(wrap_pyfunction!(open_i18n)).unwrap();
|
||||||
|
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue