translations no longer require an open collection

This commit is contained in:
Damien Elmes 2020-02-23 14:57:02 +10:00
parent 4430c67069
commit 0e931808c9
7 changed files with 141 additions and 17 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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()
)

View file

@ -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
View 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 枚の復習カード"

View file

@ -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)
}
}

View file

@ -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();