mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 12:47:11 -05:00
use marker structs to denote type of translations
This commit is contained in:
parent
1cbcd244cb
commit
df743d2633
6 changed files with 100 additions and 75 deletions
|
|
@ -32,7 +32,7 @@ use crate::platform::respawn_launcher;
|
|||
mod platform;
|
||||
|
||||
struct State {
|
||||
tr: I18n,
|
||||
tr: I18n<anki_i18n::Launcher>,
|
||||
current_version: Option<String>,
|
||||
prerelease_marker: std::path::PathBuf,
|
||||
uv_install_root: std::path::PathBuf,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn main() -> Result<()> {
|
|||
let mut map = get_ftl_data();
|
||||
check(&map);
|
||||
let mut modules = get_modules(&map);
|
||||
write_strings(&map, &modules, "strings.rs");
|
||||
write_strings(&map, &modules, "strings.rs", "All");
|
||||
|
||||
typescript::write_ts_interface(&modules)?;
|
||||
python::write_py_interface(&modules)?;
|
||||
|
|
@ -42,14 +42,11 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// allow passing in "--cfg launcher" for the launcher
|
||||
println!("cargo::rustc-check-cfg=cfg(launcher)");
|
||||
|
||||
// generate strings for the launcher
|
||||
map.iter_mut()
|
||||
.for_each(|(_, modules)| modules.retain(|module, _| module == "launcher"));
|
||||
modules.retain(|module| module.name == "launcher");
|
||||
write_strings(&map, &modules, "strings_launcher.rs");
|
||||
write_strings(&map, &modules, "strings_launcher.rs", "Launcher");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
// Include auto-generated content
|
||||
|
||||
#![allow(clippy::all)]
|
||||
|
||||
#[cfg(not(launcher))]
|
||||
#[derive(Clone)]
|
||||
pub struct All;
|
||||
|
||||
// Include auto-generated content
|
||||
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
|
||||
|
||||
#[cfg(launcher)]
|
||||
include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs"));
|
||||
impl Translations for All {
|
||||
const _STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &STRINGS;
|
||||
const _KEYS_BY_MODULE: &[&[&str]] = &KEYS_BY_MODULE;
|
||||
}
|
||||
|
|
|
|||
15
rslib/i18n/src/generated_launcher.rs
Normal file
15
rslib/i18n/src/generated_launcher.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
#![allow(clippy::all)]
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Launcher;
|
||||
|
||||
// Include auto-generated content
|
||||
include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs"));
|
||||
|
||||
impl Translations for Launcher {
|
||||
const _STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &STRINGS;
|
||||
const _KEYS_BY_MODULE: &[&[&str]] = &KEYS_BY_MODULE;
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod generated;
|
||||
mod generated_launcher;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
|
@ -12,8 +14,6 @@ use fluent::FluentArgs;
|
|||
use fluent::FluentResource;
|
||||
use fluent::FluentValue;
|
||||
use fluent_bundle::bundle::FluentBundle as FluentBundleOrig;
|
||||
use generated::KEYS_BY_MODULE;
|
||||
use generated::STRINGS;
|
||||
use num_format::Locale;
|
||||
use serde::Serialize;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
|
@ -22,6 +22,9 @@ type FluentBundle<T> = FluentBundleOrig<T, intl_memoizer::concurrent::IntlLangMe
|
|||
|
||||
pub use fluent::fluent_args as tr_args;
|
||||
|
||||
pub use crate::generated::All;
|
||||
pub use crate::generated_launcher::Launcher;
|
||||
|
||||
pub trait Number: Into<FluentNumber> {
|
||||
fn round(self) -> Self;
|
||||
}
|
||||
|
|
@ -187,20 +190,67 @@ fn get_bundle_with_extra(
|
|||
get_bundle(text, extra_text, &locales)
|
||||
}
|
||||
|
||||
pub trait Translations {
|
||||
const _STRINGS: &phf::Map<&str, &phf::Map<&str, &str>>;
|
||||
const _KEYS_BY_MODULE: &[&[&str]];
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct I18n {
|
||||
pub struct I18n<P: Translations = All> {
|
||||
inner: Arc<Mutex<I18nInner>>,
|
||||
_translations_type: std::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
||||
KEYS_BY_MODULE
|
||||
.get(module_idx)
|
||||
.and_then(|translations| translations.get(translation_idx))
|
||||
.cloned()
|
||||
.unwrap_or("invalid-module-or-translation-index")
|
||||
}
|
||||
impl<P: Translations> I18n<P> {
|
||||
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
||||
P::_KEYS_BY_MODULE
|
||||
.get(module_idx)
|
||||
.and_then(|translations| translations.get(translation_idx))
|
||||
.cloned()
|
||||
.unwrap_or("invalid-module-or-translation-index")
|
||||
}
|
||||
|
||||
fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec<String> {
|
||||
langs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|lang| {
|
||||
let mut buf = String::new();
|
||||
let lang_name = remapped_lang_name(&lang);
|
||||
if let Some(strings) = P::_STRINGS.get(lang_name) {
|
||||
if desired_modules.is_empty() {
|
||||
// empty list, provide all modules
|
||||
for value in strings.values() {
|
||||
buf.push_str(value)
|
||||
}
|
||||
} else {
|
||||
for module_name in desired_modules {
|
||||
if let Some(text) = strings.get(module_name.as_str()) {
|
||||
buf.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// This temporarily behaves like the older code; in the future we could
|
||||
/// either access each &str separately, or load them on demand.
|
||||
fn ftl_localized_text(lang: &LanguageIdentifier) -> Option<String> {
|
||||
let lang = remapped_lang_name(lang);
|
||||
if let Some(module) = P::_STRINGS.get(lang) {
|
||||
let mut text = String::new();
|
||||
for module_text in module.values() {
|
||||
text.push_str(module_text)
|
||||
}
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
pub fn template_only() -> Self {
|
||||
Self::new::<&str>(&[])
|
||||
}
|
||||
|
|
@ -225,7 +275,7 @@ impl I18n {
|
|||
let mut output_langs = vec![];
|
||||
for lang in input_langs {
|
||||
// if the language is bundled in the binary
|
||||
if let Some(text) = ftl_localized_text(&lang).or_else(|| {
|
||||
if let Some(text) = Self::ftl_localized_text(&lang).or_else(|| {
|
||||
// when testing, allow missing translations
|
||||
if cfg!(test) {
|
||||
Some(String::new())
|
||||
|
|
@ -244,7 +294,7 @@ impl I18n {
|
|||
|
||||
// add English templates
|
||||
let template_lang = "en-US".parse().unwrap();
|
||||
let template_text = ftl_localized_text(&template_lang).unwrap();
|
||||
let template_text = Self::ftl_localized_text(&template_lang).unwrap();
|
||||
let template_bundle = get_bundle_with_extra(&template_text, None).unwrap();
|
||||
bundles.push(template_bundle);
|
||||
output_langs.push(template_lang);
|
||||
|
|
@ -261,6 +311,7 @@ impl I18n {
|
|||
bundles,
|
||||
langs: output_langs,
|
||||
})),
|
||||
_translations_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +321,7 @@ impl I18n {
|
|||
message_index: usize,
|
||||
args: FluentArgs,
|
||||
) -> String {
|
||||
let key = get_key(module_index, message_index);
|
||||
let key = Self::get_key(module_index, message_index);
|
||||
self.translate(key, Some(args)).into()
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +356,7 @@ impl I18n {
|
|||
/// implementation.
|
||||
pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
let resources = get_modules(&inner.langs, desired_modules);
|
||||
let resources = Self::get_modules(&inner.langs, desired_modules);
|
||||
ResourcesForJavascript {
|
||||
langs: inner.langs.iter().map(ToString::to_string).collect(),
|
||||
resources,
|
||||
|
|
@ -313,47 +364,6 @@ impl I18n {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec<String> {
|
||||
langs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|lang| {
|
||||
let mut buf = String::new();
|
||||
let lang_name = remapped_lang_name(&lang);
|
||||
if let Some(strings) = STRINGS.get(lang_name) {
|
||||
if desired_modules.is_empty() {
|
||||
// empty list, provide all modules
|
||||
for value in strings.values() {
|
||||
buf.push_str(value)
|
||||
}
|
||||
} else {
|
||||
for module_name in desired_modules {
|
||||
if let Some(text) = strings.get(module_name.as_str()) {
|
||||
buf.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// This temporarily behaves like the older code; in the future we could either
|
||||
/// access each &str separately, or load them on demand.
|
||||
fn ftl_localized_text(lang: &LanguageIdentifier) -> Option<String> {
|
||||
let lang = remapped_lang_name(lang);
|
||||
if let Some(module) = STRINGS.get(lang) {
|
||||
let mut text = String::new();
|
||||
for module_text in module.values() {
|
||||
text.push_str(module_text)
|
||||
}
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct I18nInner {
|
||||
// bundles in preferred language order, with template English as the
|
||||
// last element
|
||||
|
|
@ -490,7 +500,7 @@ mod test {
|
|||
#[test]
|
||||
fn i18n() {
|
||||
// English template
|
||||
let tr = I18n::new(&["zz"]);
|
||||
let tr = I18n::<All>::new(&["zz"]);
|
||||
assert_eq!(tr.translate("valid-key", None), "a valid key");
|
||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||
|
||||
|
|
@ -513,7 +523,7 @@ mod test {
|
|||
);
|
||||
|
||||
// Another language
|
||||
let tr = I18n::new(&["ja_JP"]);
|
||||
let tr = I18n::<All>::new(&["ja_JP"]);
|
||||
assert_eq!(tr.translate("valid-key", None), "キー");
|
||||
assert_eq!(tr.translate("only-in-english", None), "not translated");
|
||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||
|
|
@ -524,7 +534,7 @@ mod test {
|
|||
);
|
||||
|
||||
// Decimal separator
|
||||
let tr = I18n::new(&["pl-PL"]);
|
||||
let tr = I18n::<All>::new(&["pl-PL"]);
|
||||
// Polish will use a comma if the string is translated
|
||||
assert_eq!(
|
||||
tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::extract::VariableKind;
|
|||
use crate::gather::TranslationsByFile;
|
||||
use crate::gather::TranslationsByLang;
|
||||
|
||||
pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str) {
|
||||
pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str, tag: &str) {
|
||||
let mut buf = String::new();
|
||||
|
||||
// lang->module map
|
||||
|
|
@ -25,25 +25,25 @@ pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str)
|
|||
// ordered list of translations by module
|
||||
write_translation_key_index(modules, &mut buf);
|
||||
// methods to generate messages
|
||||
write_methods(modules, &mut buf);
|
||||
write_methods(modules, &mut buf, tag);
|
||||
|
||||
let dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
let path = dir.join(out_fn);
|
||||
fs::write(path, buf).unwrap();
|
||||
}
|
||||
|
||||
fn write_methods(modules: &[Module], buf: &mut String) {
|
||||
fn write_methods(modules: &[Module], buf: &mut String, tag: &str) {
|
||||
buf.push_str(
|
||||
r#"
|
||||
#[allow(unused_imports)]
|
||||
use crate::{I18n,Number};
|
||||
use crate::{I18n,Number,Translations};
|
||||
#[allow(unused_imports)]
|
||||
use fluent::{FluentValue, FluentArgs};
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl I18n {
|
||||
"#,
|
||||
);
|
||||
writeln!(buf, "impl I18n<{tag}> {{").unwrap();
|
||||
for module in modules {
|
||||
for translation in &module.translations {
|
||||
let func = translation.key.to_snake_case();
|
||||
|
|
|
|||
Loading…
Reference in a new issue