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
// 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 is_default(&self) -> bool {
true
}
fn is_check(&self) -> bool {
true
}
fn severity(&self) -> Severity {
Severity::Warning
}
/// 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 with severity [`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 * 10 <= len_msgstr || (len_msgid == 1 && len_msgstr > 1) {
vec![
self.new_diag(
checker,
format!("translation too long ({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_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 " :"
msgstr " ... :"
msgid "ok"
msgstr "ok, ceci est une traduction trop longue pour test"
"#,
);
assert_eq!(diags.len(), 2);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Warning);
assert_eq!(diag.message, "translation too long (1 / 5)");
let diag = &diags[1];
assert_eq!(diag.severity, Severity::Warning);
assert_eq!(diag.message, "translation too long (2 / 49)");
}
}