gobject_linter/rules/
untranslated_string.rs1use gobject_ast::model::{Expression, FileModel, FunctionDefItem};
2
3use crate::{
4 ast_context::AstContext,
5 config::Config,
6 rules::{Category, Rule, Violation},
7};
8
9pub struct UntranslatedString;
10
11impl Rule for UntranslatedString {
12 fn name(&self) -> &'static str {
13 "untranslated_string"
14 }
15
16 fn description(&self) -> &'static str {
17 "Detect user-visible strings that should be wrapped with gettext"
18 }
19
20 fn category(&self) -> Category {
21 Category::Pedantic
22 }
23
24 fn check_func_impl(
25 &self,
26 _ast_context: &AstContext,
27 _config: &Config,
28 func: &FunctionDefItem,
29 file: &FileModel,
30 violations: &mut Vec<Violation>,
31 ) {
32 for call in func.find_calls_matching(|name| self.get_translatable_param(name).is_some()) {
33 let Some(func_name) = call.function_name_str() else {
34 continue;
35 };
36 let Some(arg_index) = self.get_translatable_param(func_name) else {
37 continue;
38 };
39 let Some(arg) = call.arguments.get(arg_index) else {
40 continue;
41 };
42 self.check_argument(arg, func_name, file, violations);
43 }
44 }
45}
46
47impl UntranslatedString {
48 fn check_argument(
49 &self,
50 arg: &Expression,
51 func_name: &str,
52 file: &FileModel,
53 violations: &mut Vec<Violation>,
54 ) {
55 if let Expression::Call(call) = arg
56 && let Some(name) = call.function_name_str()
57 && matches!(name, "_" | "gettext" | "N_" | "g_dgettext" | "g_dpgettext2")
58 {
59 return;
60 }
61
62 if let Expression::StringLiteral(_) = arg {
64 if let Some(string_value) = arg.extract_string_value() {
66 if string_value.trim().is_empty() {
67 return;
68 }
69 if !string_value.chars().any(char::is_alphabetic) {
72 return;
73 }
74 if matches!(string_value.as_bytes(), [b'%', c] if c.is_ascii_alphabetic()) {
76 return;
77 }
78 }
79
80 let location = arg.location();
81 violations.push(self.violation_at(
82 &file.path,
83 location,
84 format!(
85 "User-visible string in {}() should be wrapped with _(\"...\")",
86 func_name
87 ),
88 ));
89 }
90 }
91
92 fn get_translatable_param(&self, func_name: &str) -> Option<usize> {
94 match func_name {
95 "gtk_label_new" => Some(0),
96 "gtk_label_set_text" | "gtk_label_set_markup" | "gtk_label_set_label" => Some(1),
97
98 "gtk_button_new_with_label" | "gtk_button_new_with_mnemonic" => Some(0),
99 "gtk_button_set_label" => Some(1),
100
101 "gtk_window_set_title" => Some(1),
102
103 "gtk_header_bar_set_title" | "gtk_header_bar_set_subtitle" => Some(1),
104
105 "gtk_check_button_new_with_label" | "gtk_check_button_new_with_mnemonic" => Some(0),
106
107 "gtk_radio_button_new_with_label" | "gtk_radio_button_new_with_mnemonic" => Some(1),
108
109 "gtk_entry_set_placeholder_text" | "gtk_entry_set_text" => Some(1),
110
111 "gtk_dialog_add_button" => Some(1),
112
113 "gtk_message_dialog_new" => Some(4),
114 "gtk_message_dialog_set_markup" => Some(1),
115
116 "adw_message_dialog_new"
117 | "adw_message_dialog_set_heading"
118 | "adw_message_dialog_set_body"
119 | "adw_message_dialog_set_body_use_markup" => Some(1),
120 "adw_message_dialog_add_response" => Some(2),
121
122 "adw_status_page_set_title" | "adw_status_page_set_description" => Some(1),
123
124 "adw_toast_new" => Some(0),
125 "adw_toast_set_title" | "adw_toast_set_button_label" => Some(1),
126
127 "adw_preferences_group_set_title" | "adw_preferences_group_set_description" => Some(1),
128
129 "adw_preferences_row_set_title" => Some(1),
130
131 "adw_action_row_set_title" | "adw_action_row_set_subtitle" => Some(1),
132
133 "adw_entry_row_set_title" => Some(1),
134
135 "adw_combo_row_set_title" => Some(1),
136
137 "adw_expander_row_set_title" | "adw_expander_row_set_subtitle" => Some(1),
138
139 "adw_window_title_new" => Some(0),
140 "adw_window_title_set_title" | "adw_window_title_set_subtitle" => Some(1),
141
142 _ => None,
143 }
144 }
145}