mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02: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;
|
||||
}
|
||||
|
||||
message I18nBackendInit {
|
||||
repeated string preferred_langs = 4;
|
||||
string locale_folder_path = 5;
|
||||
}
|
||||
|
||||
// 1-15 reserved for future use; 2047 for errors
|
||||
|
||||
message BackendInput {
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
|
||||
import anki
|
||||
|
||||
langs = sorted(
|
||||
[
|
||||
("Afrikaans", "af_ZA"),
|
||||
|
@ -134,11 +138,14 @@ def lang_to_disk_lang(lang: str) -> str:
|
|||
# the currently set interface language
|
||||
currentLang = "en"
|
||||
|
||||
# the current translation catalog
|
||||
# the current gettext translation catalog
|
||||
current_catalog: Optional[
|
||||
Union[gettext.NullTranslations, gettext.GNUTranslations]
|
||||
] = None
|
||||
|
||||
# the current Fluent translation instance
|
||||
current_i18n: Optional[anki.rsbackend.I18nBackend]
|
||||
|
||||
# path to 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:
|
||||
global currentLang, current_catalog, locale_folder
|
||||
global currentLang, current_catalog, current_i18n, locale_folder
|
||||
gettext_dir = os.path.join(locale_dir, "gettext")
|
||||
ftl_dir = os.path.join(locale_dir, "fluent")
|
||||
|
||||
currentLang = lang
|
||||
current_catalog = gettext.translation(
|
||||
"anki", gettext_dir, languages=[lang], fallback=True
|
||||
)
|
||||
current_i18n = anki.rsbackend.I18nBackend(
|
||||
preferred_langs=[lang], ftl_folder=ftl_dir
|
||||
)
|
||||
locale_folder = locale_dir
|
||||
|
||||
|
||||
|
|
|
@ -329,15 +329,8 @@ class RustBackend:
|
|||
)
|
||||
|
||||
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(
|
||||
pb.BackendInput(translate_string=pb.TranslateStringIn(key=key, args=args))
|
||||
pb.BackendInput(translate_string=translate_string_in(key, **kwargs))
|
||||
).translate_string
|
||||
|
||||
def format_time_span(
|
||||
|
@ -366,3 +359,28 @@ class RustBackend:
|
|||
)
|
||||
)
|
||||
).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
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
from anki.lang import _
|
||||
from anki.rsbackend import FString
|
||||
|
@ -32,12 +33,8 @@ def locale_dir() -> str:
|
|||
|
||||
|
||||
def tr(key: FString, **kwargs: Union[str, int, float]) -> str:
|
||||
"""Shortcut to access translations from the backend.
|
||||
(Currently) requires an open collection."""
|
||||
if aqt.mw.col:
|
||||
return aqt.mw.col.backend.translate(key, **kwargs)
|
||||
else:
|
||||
return repr(key)
|
||||
"Shortcut to access Fluent translations."
|
||||
return anki.lang.current_i18n.translate(key, *kwargs)
|
||||
|
||||
|
||||
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
|
||||
// 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 pyo3::prelude::*;
|
||||
use pyo3::types::PyBytes;
|
||||
use pyo3::{exceptions, wrap_pyfunction};
|
||||
|
||||
// Regular backend
|
||||
//////////////////////////////////
|
||||
|
||||
#[pyclass]
|
||||
struct Backend {
|
||||
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]
|
||||
fn ankirspy(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Backend>()?;
|
||||
m.add_wrapped(wrap_pyfunction!(buildhash)).unwrap();
|
||||
m.add_wrapped(wrap_pyfunction!(open_backend)).unwrap();
|
||||
m.add_wrapped(wrap_pyfunction!(open_i18n)).unwrap();
|
||||
|
||||
env_logger::init();
|
||||
|
||||
|
|
Loading…
Reference in a new issue