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
// SPDX-FileCopyrightText: 2026 Sébastien Helleu <flashcode@flashtux.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! Implementation of the `long` rule: check if translation is too long.
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 LongRule;
impl RuleChecker for LongRule {
fn name(&self) -> &'static str {
"long"
}
fn description(&self) -> &'static str {
"Check if translation is too long compared to source."
}
fn is_default(&self) -> bool {
true
}
fn is_check(&self) -> bool {
true
}
/// Check for too long translation.
///
/// This rule reports the entry if one of both conditions is met (leading and trailing
/// whitespace in strings are ignored):
///
/// - The translation has at least 10 times more UTF-8 characters than the source.
/// - The source has one UTF-8 character and the translation has more than one character.
///
/// Wrong entry:
/// ```text
/// msgid " :"
/// msgstr " ... :"
/// ```
///
/// Correct entry:
/// ```text
/// msgid "ok"
/// msgstr "ok, ceci est une traduction trop longue pour test"
/// ```
///
/// Diagnostics reported:
/// - [`warning`](Severity::Warning): `translation too long (# / #)`
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_msgid * checker.config.check.long_factor as usize <= len_msgstr {
self.new_diag(
checker,
Severity::Warning,
format!("translation too long ({len_msgid} / {len_msgstr})"),
)
.map(|d| d.with_msgs(msgid, msgstr))
.into_iter()
.collect()
} else {
vec![]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{diagnostic::Diagnostic, rules::rule::Rules};
fn check_long(content: &str) -> Vec<Diagnostic> {
let mut checker = Checker::new(content.as_bytes());
let rules = Rules::new(vec![Box::new(LongRule {})]);
checker.do_all_checks(&rules);
checker.diagnostics
}
#[test]
fn test_no_long() {
let diags = check_long(
r#"
msgid "tested"
msgstr "testé"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_long_error_noqa() {
let diags = check_long(
r#"
#, noqa:long
msgid " :"
msgstr " ... :"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_long_error() {
let diags = check_long(
r#"
msgid "ok"
msgstr "ok, ceci est un long message"
"#,
);
assert_eq!(diags.len(), 1);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Warning);
assert_eq!(diag.message, "translation too long (2 / 28)");
}
}