diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index b2b99f73c0..f1a99ef939 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -36,6 +36,8 @@ decks-repeat-failed-cards-after = Delay Repeat failed cards after decks-study = Study decks-study-deck = Study Deck decks-filtered-deck-search-empty = No cards matched the provided search. Some cards may have been excluded because they are in a different filtered deck, or suspended. +decks-choose-deck = Choose Deck +decks-target-deck-ctrlandd = Target Deck (Ctrl+D) ## Sort order of cards diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl index f7eccd08b7..5eac044853 100644 --- a/ftl/core/notetypes.ftl +++ b/ftl/core/notetypes.ftl @@ -1,4 +1,6 @@ notetypes-notetype = Note Type +notetypes-choose-note-type = Choose Note Type +notetypes-change-note-type-ctrlandn = Change Note Type (Ctrl+N) ## Default field names in newly created note types diff --git a/rslib/i18n/check.rs b/rslib/i18n/check.rs index 48521bbdd7..6f6af0e815 100644 --- a/rslib/i18n/check.rs +++ b/rslib/i18n/check.rs @@ -12,8 +12,10 @@ use super::gather::TranslationsByLang; pub fn check(lang_map: &TranslationsByLang) { for (lang, files_map) in lang_map { - for (fname, content) in files_map { - check_content(lang, fname, content); + for (fname, repos) in files_map { + for content in repos.values() { + check_content(lang, fname, content); + } } } } diff --git a/rslib/i18n/extract.rs b/rslib/i18n/extract.rs index 60efd596c3..66d0bedc9c 100644 --- a/rslib/i18n/extract.rs +++ b/rslib/i18n/extract.rs @@ -2,7 +2,9 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::collections::HashSet; +use std::fmt::Display; use std::fmt::Write; +use std::marker::PhantomData; use fluent_syntax::ast::Entry; use fluent_syntax::ast::Expression; @@ -13,6 +15,8 @@ use fluent_syntax::parser::parse; use serde::Serialize; use crate::gather::TranslationsByLang; +use crate::gather::TranslationsByRepo; + #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Serialize)] pub struct Module { pub name: String, @@ -23,6 +27,7 @@ pub struct Module { #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Serialize)] pub struct Translation { pub key: String, + pub repo: String, pub text: String, pub variables: Vec, pub index: usize, @@ -42,13 +47,19 @@ pub enum VariableKind { Any, } +impl Translation { + pub fn is_core(&self) -> bool { + !["qt", "mobile"].contains(&self.repo.as_str()) + } +} + pub fn get_modules(data: &TranslationsByLang) -> Vec { let mut output = vec![]; - for (module, text) in &data["templates"] { + for (module, translations_by_repo) in &data["templates"] { output.push(Module { name: module.to_string(), - translations: extract_metadata(text), + translations: extract_metadata(translations_by_repo), index: 0, }); } @@ -65,38 +76,39 @@ pub fn get_modules(data: &TranslationsByLang) -> Vec { output } -fn extract_metadata(ftl_text: &str) -> Vec { - let res = parse(ftl_text).unwrap(); +fn extract_metadata(translations_by_repo: &TranslationsByRepo) -> Vec { let mut output = vec![]; + for (repo, ftl_text) in translations_by_repo.iter() { + let res = parse(ftl_text.clone()).unwrap(); + for entry in res.body { + if let Entry::Message(m) = entry { + if let Some(pattern) = m.value { + let mut visitor = Visitor::default(); + visitor.visit_pattern(&pattern); + let key = m.id.name.to_string(); - for entry in res.body { - if let Entry::Message(m) = entry { - if let Some(pattern) = m.value { - let mut visitor = Visitor::default(); - visitor.visit_pattern(&pattern); - let key = m.id.name.to_string(); + // special case translations that were ported from gettext, and use embedded + // terms that reference other variables that aren't visible to our visitor + if key == "statistics-studied-today" { + visitor.variables.push("amount".to_string()); + visitor.variables.push("cards".to_string()); + } else if key == "statistics-average-answer-time" { + visitor.variables.push("cards-per-minute".to_string()); + } - // special case translations that were ported from gettext, and use embedded - // terms that reference other variables that aren't visible to our visitor - if key == "statistics-studied-today" { - visitor.variables.push("amount".to_string()); - visitor.variables.push("cards".to_string()); - } else if key == "statistics-average-answer-time" { - visitor.variables.push("cards-per-minute".to_string()); + let (text, variables) = visitor.into_output(); + + output.push(Translation { + key, + repo: repo.clone(), + text, + variables, + index: 0, + }) } - - let (text, variables) = visitor.into_output(); - - output.push(Translation { - key, - text, - variables, - index: 0, - }) } } } - output.sort_unstable(); output @@ -104,12 +116,16 @@ fn extract_metadata(ftl_text: &str) -> Vec { /// Gather variable names and (rough) text from Fluent AST. #[derive(Default)] -struct Visitor { +struct Visitor { text: String, variables: Vec, + _phantom: PhantomData, } -impl Visitor { +impl Visitor +where + T: AsRef + Display, +{ fn into_output(self) -> (String, Vec) { // make unique, preserving order let mut seen = HashSet::new(); @@ -128,16 +144,16 @@ impl Visitor { (self.text, vars) } - fn visit_pattern(&mut self, pattern: &Pattern<&str>) { + fn visit_pattern(&mut self, pattern: &Pattern) { for element in &pattern.elements { match element { - PatternElement::TextElement { value } => self.text.push_str(value), + PatternElement::TextElement { value } => self.text.push_str(value.as_ref()), PatternElement::Placeable { expression } => self.visit_expression(expression), } } } - fn visit_inline_expression(&mut self, expr: &InlineExpression<&str>, in_select: bool) { + fn visit_inline_expression(&mut self, expr: &InlineExpression, in_select: bool) { match expr { InlineExpression::VariableReference { id } => { if !in_select { @@ -152,7 +168,7 @@ impl Visitor { } } - fn visit_expression(&mut self, expression: &Expression<&str>) { + fn visit_expression(&mut self, expression: &Expression) { match expression { Expression::Select { selector, variants } => { self.visit_inline_expression(selector, true); diff --git a/rslib/i18n/gather.rs b/rslib/i18n/gather.rs index 835e108af8..d1aa6af499 100644 --- a/rslib/i18n/gather.rs +++ b/rslib/i18n/gather.rs @@ -10,7 +10,8 @@ use std::fs; use std::path::Path; use std::path::PathBuf; -pub type TranslationsByFile = HashMap; +pub type TranslationsByRepo = HashMap; +pub type TranslationsByFile = HashMap; pub type TranslationsByLang = HashMap; /// Read the contents of the FTL files into a TranslationMap structure. @@ -19,24 +20,29 @@ pub fn get_ftl_data() -> TranslationsByLang { // English core templates are taken from this repo let ftl_base = source_tree_root(); - add_folder(&mut map, &ftl_base.join("core"), "templates"); + add_folder(&mut map, &ftl_base.join("core"), "core", "templates"); // And core translations from submodule - add_translation_root(&mut map, &ftl_base.join("core-repo/core"), true); + add_translation_root(&mut map, &ftl_base.join("core-repo/core"), "core", true); if let Some(path) = extra_ftl_root() { // Mobile client has requested its own extra translations - add_translation_root(&mut map, &path, false); + add_translation_root( + &mut map, + &path, + &path.file_name().unwrap().to_string_lossy(), + false, + ); } else { // Qt core templates from this repo - add_folder(&mut map, &ftl_base.join("qt"), "templates"); + add_folder(&mut map, &ftl_base.join("qt"), "qt", "templates"); // And translations from submodule - add_translation_root(&mut map, &ftl_base.join("qt-repo/desktop"), true) + add_translation_root(&mut map, &ftl_base.join("qt-repo/desktop"), "qt", true) } map } /// For each .ftl file in the provided folder, add it to the translation map. -fn add_folder(map: &mut TranslationsByLang, folder: &Path, lang: &str) { +fn add_folder(map: &mut TranslationsByLang, folder: &Path, repo: &str, lang: &str) { let map_entry = map.entry(lang.to_string()).or_default(); for entry in fs::read_dir(folder).unwrap() { let entry = entry.unwrap(); @@ -50,7 +56,12 @@ fn add_folder(map: &mut TranslationsByLang, folder: &Path, lang: &str) { text.ends_with('\n'), "file was missing final newline: {entry:?}" ); - map_entry.entry(module).or_default().push_str(&text); + map_entry + .entry(module) + .or_default() + .entry(repo.to_string()) + .or_default() + .push_str(&text); println!("cargo:rerun-if-changed={}", entry.path().to_str().unwrap()); } } @@ -58,14 +69,19 @@ fn add_folder(map: &mut TranslationsByLang, folder: &Path, lang: &str) { /// For each language folder in `root`, add the ftl files stored inside. /// If ignore_templates is true, the templates/ folder will be ignored, on the /// assumption the templates were extracted from the source tree. -fn add_translation_root(map: &mut TranslationsByLang, root: &Path, ignore_templates: bool) { +fn add_translation_root( + map: &mut TranslationsByLang, + root: &Path, + repo: &str, + ignore_templates: bool, +) { for entry in fs::read_dir(root).unwrap() { let entry = entry.unwrap(); let lang = entry.file_name().to_string_lossy().to_string(); if ignore_templates && lang == "templates" { continue; } - add_folder(map, &entry.path(), &lang); + add_folder(map, &entry.path(), repo, &lang); } } diff --git a/rslib/i18n/typescript.rs b/rslib/i18n/typescript.rs index 0f483cb586..dc6e4ebdb3 100644 --- a/rslib/i18n/typescript.rs +++ b/rslib/i18n/typescript.rs @@ -42,7 +42,7 @@ fn render_module_map(modules: &[Module], ts_out: &mut String) { fn render_methods(modules: &[Module], ts_out: &mut String) { for module in modules { - for translation in &module.translations { + for translation in module.translations.iter().filter(|t| t.is_core()) { let text = &translation.text; let key = &translation.key; let func_name = key.replace('-', "_").to_camel_case(); diff --git a/rslib/i18n/write_strings.rs b/rslib/i18n/write_strings.rs index db31be2b78..007bd5afa8 100644 --- a/rslib/i18n/write_strings.rs +++ b/rslib/i18n/write_strings.rs @@ -8,6 +8,7 @@ use std::fs; use std::path::PathBuf; use inflections::Inflect; +use itertools::Itertools; use crate::extract::Module; use crate::extract::Translation; @@ -45,7 +46,7 @@ use std::borrow::Cow; ); writeln!(buf, "impl I18n<{tag}> {{").unwrap(); for module in modules { - for translation in &module.translations { + for translation in module.translations.iter().filter(|t| t.is_core()) { let func = translation.key.to_snake_case(); let key = &translation.key; let doc = translation.text.replace('\n', " "); @@ -196,8 +197,9 @@ pub(crate) const {lang_name}: phf::Map<&str, &str> = phf::phf_map! {{", ) .unwrap(); - for (module, contents) in modules { - let escaped_contents = escape_unicode_control_chars(contents); + for (module, repos) in modules { + let contents = repos.values().join(""); + let escaped_contents = escape_unicode_control_chars(&contents); writeln!( buf, r###" "{module}" => r##"{escaped_contents}"##,"### diff --git a/ts/lib/components/DeckChooser.svelte b/ts/lib/components/DeckChooser.svelte index f9c6ba5419..de8df0bf19 100644 --- a/ts/lib/components/DeckChooser.svelte +++ b/ts/lib/components/DeckChooser.svelte @@ -53,11 +53,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/lib/components/NotetypeChooser.svelte b/ts/lib/components/NotetypeChooser.svelte index 0952c5029c..39906df2bd 100644 --- a/ts/lib/components/NotetypeChooser.svelte +++ b/ts/lib/components/NotetypeChooser.svelte @@ -53,11 +53,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html