mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 20:57:13 -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;
|
mod platform;
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
tr: I18n,
|
tr: I18n<anki_i18n::Launcher>,
|
||||||
current_version: Option<String>,
|
current_version: Option<String>,
|
||||||
prerelease_marker: std::path::PathBuf,
|
prerelease_marker: std::path::PathBuf,
|
||||||
uv_install_root: std::path::PathBuf,
|
uv_install_root: std::path::PathBuf,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ fn main() -> Result<()> {
|
||||||
let mut map = get_ftl_data();
|
let mut map = get_ftl_data();
|
||||||
check(&map);
|
check(&map);
|
||||||
let mut modules = get_modules(&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)?;
|
typescript::write_ts_interface(&modules)?;
|
||||||
python::write_py_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
|
// generate strings for the launcher
|
||||||
map.iter_mut()
|
map.iter_mut()
|
||||||
.for_each(|(_, modules)| modules.retain(|module, _| module == "launcher"));
|
.for_each(|(_, modules)| modules.retain(|module, _| module == "launcher"));
|
||||||
modules.retain(|module| module.name == "launcher");
|
modules.retain(|module| module.name == "launcher");
|
||||||
write_strings(&map, &modules, "strings_launcher.rs");
|
write_strings(&map, &modules, "strings_launcher.rs", "Launcher");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
// 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
|
||||||
|
|
||||||
// Include auto-generated content
|
|
||||||
|
|
||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
#[cfg(not(launcher))]
|
#[derive(Clone)]
|
||||||
|
pub struct All;
|
||||||
|
|
||||||
|
// Include auto-generated content
|
||||||
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
|
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
|
||||||
|
|
||||||
#[cfg(launcher)]
|
impl Translations for All {
|
||||||
include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs"));
|
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
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod generated;
|
mod generated;
|
||||||
|
mod generated_launcher;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
|
@ -12,8 +14,6 @@ use fluent::FluentArgs;
|
||||||
use fluent::FluentResource;
|
use fluent::FluentResource;
|
||||||
use fluent::FluentValue;
|
use fluent::FluentValue;
|
||||||
use fluent_bundle::bundle::FluentBundle as FluentBundleOrig;
|
use fluent_bundle::bundle::FluentBundle as FluentBundleOrig;
|
||||||
use generated::KEYS_BY_MODULE;
|
|
||||||
use generated::STRINGS;
|
|
||||||
use num_format::Locale;
|
use num_format::Locale;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use unic_langid::LanguageIdentifier;
|
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 fluent::fluent_args as tr_args;
|
||||||
|
|
||||||
|
pub use crate::generated::All;
|
||||||
|
pub use crate::generated_launcher::Launcher;
|
||||||
|
|
||||||
pub trait Number: Into<FluentNumber> {
|
pub trait Number: Into<FluentNumber> {
|
||||||
fn round(self) -> Self;
|
fn round(self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
@ -187,20 +190,67 @@ fn get_bundle_with_extra(
|
||||||
get_bundle(text, extra_text, &locales)
|
get_bundle(text, extra_text, &locales)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub trait Translations {
|
||||||
pub struct I18n {
|
const _STRINGS: &phf::Map<&str, &phf::Map<&str, &str>>;
|
||||||
inner: Arc<Mutex<I18nInner>>,
|
const _KEYS_BY_MODULE: &[&[&str]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct I18n<P: Translations = All> {
|
||||||
|
inner: Arc<Mutex<I18nInner>>,
|
||||||
|
_translations_type: std::marker::PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Translations> I18n<P> {
|
||||||
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
||||||
KEYS_BY_MODULE
|
P::_KEYS_BY_MODULE
|
||||||
.get(module_idx)
|
.get(module_idx)
|
||||||
.and_then(|translations| translations.get(translation_idx))
|
.and_then(|translations| translations.get(translation_idx))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or("invalid-module-or-translation-index")
|
.unwrap_or("invalid-module-or-translation-index")
|
||||||
}
|
}
|
||||||
|
|
||||||
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) = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn template_only() -> Self {
|
pub fn template_only() -> Self {
|
||||||
Self::new::<&str>(&[])
|
Self::new::<&str>(&[])
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +275,7 @@ impl I18n {
|
||||||
let mut output_langs = vec![];
|
let mut output_langs = vec![];
|
||||||
for lang in input_langs {
|
for lang in input_langs {
|
||||||
// if the language is bundled in the binary
|
// 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
|
// when testing, allow missing translations
|
||||||
if cfg!(test) {
|
if cfg!(test) {
|
||||||
Some(String::new())
|
Some(String::new())
|
||||||
|
|
@ -244,7 +294,7 @@ impl I18n {
|
||||||
|
|
||||||
// add English templates
|
// add English templates
|
||||||
let template_lang = "en-US".parse().unwrap();
|
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();
|
let template_bundle = get_bundle_with_extra(&template_text, None).unwrap();
|
||||||
bundles.push(template_bundle);
|
bundles.push(template_bundle);
|
||||||
output_langs.push(template_lang);
|
output_langs.push(template_lang);
|
||||||
|
|
@ -261,6 +311,7 @@ impl I18n {
|
||||||
bundles,
|
bundles,
|
||||||
langs: output_langs,
|
langs: output_langs,
|
||||||
})),
|
})),
|
||||||
|
_translations_type: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +321,7 @@ impl I18n {
|
||||||
message_index: usize,
|
message_index: usize,
|
||||||
args: FluentArgs,
|
args: FluentArgs,
|
||||||
) -> String {
|
) -> String {
|
||||||
let key = get_key(module_index, message_index);
|
let key = Self::get_key(module_index, message_index);
|
||||||
self.translate(key, Some(args)).into()
|
self.translate(key, Some(args)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,7 +356,7 @@ impl I18n {
|
||||||
/// implementation.
|
/// implementation.
|
||||||
pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript {
|
pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
let resources = get_modules(&inner.langs, desired_modules);
|
let resources = Self::get_modules(&inner.langs, desired_modules);
|
||||||
ResourcesForJavascript {
|
ResourcesForJavascript {
|
||||||
langs: inner.langs.iter().map(ToString::to_string).collect(),
|
langs: inner.langs.iter().map(ToString::to_string).collect(),
|
||||||
resources,
|
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 {
|
struct I18nInner {
|
||||||
// bundles in preferred language order, with template English as the
|
// bundles in preferred language order, with template English as the
|
||||||
// last element
|
// last element
|
||||||
|
|
@ -490,7 +500,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn i18n() {
|
fn i18n() {
|
||||||
// English template
|
// 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("valid-key", None), "a valid key");
|
||||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||||
|
|
||||||
|
|
@ -513,7 +523,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Another language
|
// 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("valid-key", None), "キー");
|
||||||
assert_eq!(tr.translate("only-in-english", None), "not translated");
|
assert_eq!(tr.translate("only-in-english", None), "not translated");
|
||||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||||
|
|
@ -524,7 +534,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decimal separator
|
// 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
|
// Polish will use a comma if the string is translated
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])),
|
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::TranslationsByFile;
|
||||||
use crate::gather::TranslationsByLang;
|
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();
|
let mut buf = String::new();
|
||||||
|
|
||||||
// lang->module map
|
// lang->module map
|
||||||
|
|
@ -25,25 +25,25 @@ pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str)
|
||||||
// ordered list of translations by module
|
// ordered list of translations by module
|
||||||
write_translation_key_index(modules, &mut buf);
|
write_translation_key_index(modules, &mut buf);
|
||||||
// methods to generate messages
|
// 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 dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||||
let path = dir.join(out_fn);
|
let path = dir.join(out_fn);
|
||||||
fs::write(path, buf).unwrap();
|
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(
|
buf.push_str(
|
||||||
r#"
|
r#"
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::{I18n,Number};
|
use crate::{I18n,Number,Translations};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use fluent::{FluentValue, FluentArgs};
|
use fluent::{FluentValue, FluentArgs};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
impl I18n {
|
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
writeln!(buf, "impl I18n<{tag}> {{").unwrap();
|
||||||
for module in modules {
|
for module in modules {
|
||||||
for translation in &module.translations {
|
for translation in &module.translations {
|
||||||
let func = translation.key.to_snake_case();
|
let func = translation.key.to_snake_case();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue