use std::collections::HashSet;
use crate::checker::Checker;
use crate::diagnostic::{Diagnostic, Severity};
use crate::po::entry::Entry;
use crate::po::format::iter::FormatWordPos;
use crate::po::message::Message;
use crate::rules::rule::RuleChecker;
pub struct ForceTransRule;
impl RuleChecker for ForceTransRule {
fn name(&self) -> &'static str {
"force-trans"
}
fn description(&self) -> &'static str {
"Check that words listed in `force-trans-file` are translated (not present in translation)."
}
fn is_default(&self) -> bool {
false
}
fn is_check(&self) -> bool {
true
}
fn check_msg(
&self,
checker: &Checker,
entry: &Entry,
msgid: &Message,
msgstr: &Message,
) -> Vec<Diagnostic> {
let Some(force_words) = checker.force_trans_words.as_ref() else {
return vec![];
};
let mut id_forms: HashSet<String> = HashSet::new();
for word in FormatWordPos::new(&msgid.value, entry.format_language) {
if force_words.contains(&word.s.to_lowercase()) {
id_forms.insert(word.s.to_string());
}
}
if id_forms.is_empty() {
return vec![];
}
let mut diags = vec![];
let mut reported: HashSet<String> = HashSet::new();
for str_word in FormatWordPos::new(&msgstr.value, entry.format_language) {
if !id_forms.contains(str_word.s) || reported.contains(str_word.s) {
continue;
}
reported.insert(str_word.s.to_string());
let id_hl: Vec<(usize, usize)> =
FormatWordPos::new(&msgid.value, entry.format_language)
.filter(|w| w.s == str_word.s)
.map(|w| (w.start, w.end))
.collect();
let str_hl: Vec<(usize, usize)> =
FormatWordPos::new(&msgstr.value, entry.format_language)
.filter(|w| w.s == str_word.s)
.map(|w| (w.start, w.end))
.collect();
diags.extend(
self.new_diag(
checker,
Severity::Warning,
format!("word '{}' must be translated", str_word.s),
)
.map(|d| d.with_msgs_hl(msgid, id_hl, msgstr, str_hl)),
);
}
diags
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::*;
use crate::{config::Config, diagnostic::Diagnostic, rules::rule::Rules};
fn write_force_file(content: &str) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::TempDir::with_prefix("poexam-force-trans-")
.expect("create force-trans temp dir");
let path = tmp.path().join("force.txt");
std::fs::write(&path, content).expect("write force-trans file");
(tmp, path)
}
fn check_force_trans(force_file: &Path, content: &str) -> Vec<Diagnostic> {
let mut config = Config::default();
config.check.force_trans_file = Some(force_file.to_path_buf());
let mut checker = Checker::new(content.as_bytes()).with_config(config);
let rules = Rules::new(vec![Box::new(ForceTransRule {})]);
checker.do_all_checks(&rules);
checker.diagnostics
}
#[test]
fn test_no_match_in_source_is_ok() {
let (_tmp, force) = write_force_file("forbidden\nstop\n");
let diags = check_force_trans(
&force,
r#"
msgid "hello world"
msgstr "bonjour le monde"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_translated_word_is_ok() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
msgid "this is forbidden"
msgstr "ceci est interdit"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_word_present_in_translation_is_flagged() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
msgid "this is forbidden"
msgstr "ceci est forbidden"
"#,
);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].severity, Severity::Warning);
assert_eq!(diags[0].message, "word 'forbidden' must be translated");
}
#[test]
fn test_case_insensitive_word_list_and_match() {
let (_tmp, force) = write_force_file("stop\n");
let diags = check_force_trans(
&force,
r#"
msgid "STOP now"
msgstr "STOP maintenant"
"#,
);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "word 'STOP' must be translated");
}
#[test]
fn test_translation_with_different_case_is_ok() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
msgid "this is Forbidden"
msgstr "ceci est forbidden"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_each_source_case_form_flagged_independently() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
msgid "Forbidden, then forbidden"
msgstr "Forbidden, puis forbidden"
"#,
);
assert_eq!(diags.len(), 2);
let mut msgs: Vec<&str> = diags.iter().map(|d| d.message.as_ref()).collect();
msgs.sort_unstable();
assert_eq!(
msgs,
vec![
"word 'Forbidden' must be translated",
"word 'forbidden' must be translated",
]
);
}
#[test]
fn test_one_diagnostic_per_word_even_with_multiple_occurrences() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
msgid "this is forbidden"
msgstr "ceci est forbidden, forbidden"
"#,
);
assert_eq!(diags.len(), 1);
let str_line = diags[0].lines.last().expect("msgstr line");
assert_eq!(str_line.highlights.len(), 2);
}
#[test]
fn test_multiple_distinct_force_words_each_reported_once() {
let (_tmp, force) = write_force_file("forbidden\nstop\n");
let diags = check_force_trans(
&force,
r#"
msgid "stop, this is forbidden"
msgstr "stop, ceci est forbidden"
"#,
);
assert_eq!(diags.len(), 2);
let mut msgs: Vec<&str> = diags.iter().map(|d| d.message.as_ref()).collect();
msgs.sort_unstable();
assert_eq!(
msgs,
vec![
"word 'forbidden' must be translated",
"word 'stop' must be translated",
]
);
}
#[test]
fn test_comments_and_blank_lines_in_force_file_are_ignored() {
let (_tmp, force) = write_force_file("# this is a comment\n\nforbidden\n # comment\n");
let diags = check_force_trans(
&force,
r#"
msgid "this is forbidden"
msgstr "ceci est forbidden"
"#,
);
assert_eq!(diags.len(), 1);
}
#[test]
fn test_noqa_suppresses_force_trans() {
let (_tmp, force) = write_force_file("forbidden\n");
let diags = check_force_trans(
&force,
r#"
#, noqa:force-trans
msgid "this is forbidden"
msgstr "ceci est forbidden"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_missing_force_file_emits_warning_and_skips_checks() {
let missing = PathBuf::from("/this/path/should/not/exist/force.txt");
let diags = check_force_trans(
&missing,
r#"
msgid "this is forbidden"
msgstr "ceci est forbidden"
"#,
);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].rule, "force-trans");
assert_eq!(diags[0].severity, Severity::Warning);
assert!(diags[0].message.contains("words file not found"));
assert!(diags[0].message.contains("force-trans rule ignored"));
assert!(diags[0].message.contains("force.txt"));
}
}