1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// SPDX-FileCopyrightText: 2026 Sébastien Helleu <flashcode@flashtux.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! Implementation of the `short` rule: check if translation is too short.
use crate::checker::Checker;
use crate::diagnostic::{Diagnostic, Severity};
use crate::po::entry::Entry;
use crate::po::message::Message;
use crate::rules::rule::RuleChecker;
pub struct ShortRule;
impl RuleChecker for ShortRule {
fn name(&self) -> &'static str {
"short"
}
fn is_default(&self) -> bool {
true
}
fn is_check(&self) -> bool {
true
}
fn severity(&self) -> Severity {
Severity::Warning
}
/// Check for too short translation.
///
/// This rule reports the entry if one of both conditions is met (leading and trailing
/// whitespace in strings are ignored):
///
/// - the source has at least 10 times more UTF-8 characters than the translation
/// - the translation has one UTF-8 character and the source has more than one character.
///
/// Wrong entry:
/// ```text
/// msgid " ... :"
/// msgstr " :"
/// ```
///
/// Correct entry:
/// ```text
/// msgid "ok, this is a very long test message"
/// msgstr "ok"
/// ```
///
/// Diagnostics reported with severity [`warning`](Severity::Warning):
/// - `translation too short (# / #)`
fn check_msg(
&self,
checker: &Checker,
_entry: &Entry,
msgid: &Message,
msgstr: &Message,
) -> Vec<Diagnostic> {
// Count the number of UTF-8 chars in both strings, ignoring leading/trailing whitespace.
let len_msgid = msgid
.value
.trim()
.as_bytes()
.iter()
.filter(|&&b| b & 0xC0 != 0x80)
.count();
if len_msgid == 0 {
return vec![];
}
let len_msgstr = msgstr
.value
.trim()
.as_bytes()
.iter()
.filter(|&&b| b & 0xC0 != 0x80)
.count();
if len_msgstr == 0 {
return vec![];
}
if len_msgstr * 10 <= len_msgid
|| (len_msgstr == 1 && len_msgid > 1 && msgid.value.chars().any(char::is_whitespace))
{
vec![
self.new_diag(
checker,
format!("translation too short ({len_msgid} / {len_msgstr})"),
)
.with_msgs(msgid, msgstr),
]
} else {
vec![]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{diagnostic::Diagnostic, rules::rule::Rules};
fn check_short(content: &str) -> Vec<Diagnostic> {
let mut checker = Checker::new(content.as_bytes());
let rules = Rules::new(vec![Box::new(ShortRule {})]);
checker.do_all_checks(&rules);
checker.diagnostics
}
#[test]
fn test_no_short() {
let diags = check_short(
r#"
msgid "tested"
msgstr "testé"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_short_error_noqa() {
let diags = check_short(
r#"
#, noqa:short
msgid " ... :"
msgstr " :"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_short_error() {
let diags = check_short(
r#"
msgid " ... :"
msgstr " :"
msgid "ok, this is a very long test message"
msgstr "ok"
"#,
);
assert_eq!(diags.len(), 2);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Warning);
assert_eq!(diag.message, "translation too short (5 / 1)");
let diag = &diags[1];
assert_eq!(diag.severity, Severity::Warning);
assert_eq!(diag.message, "translation too short (36 / 2)");
}
}