use gobject_ast::{Argument, Expression, Statement};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Category, Rule, Violation},
};
pub struct UntranslatedString;
impl Rule for UntranslatedString {
fn name(&self) -> &'static str {
"untranslated_string"
}
fn description(&self) -> &'static str {
"Detect user-visible strings that should be wrapped with gettext"
}
fn category(&self) -> Category {
Category::Pedantic
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &gobject_ast::top_level::FunctionDefItem,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
self.check_statements(&func.body_statements, file, violations);
}
}
impl UntranslatedString {
fn check_statements(
&self,
statements: &[Statement],
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
for stmt in statements {
stmt.walk(&mut |s| {
s.walk_expressions(&mut |expr| {
expr.walk(&mut |e| {
self.check_expression(e, file, violations);
});
});
});
}
}
fn check_expression(
&self,
expr: &Expression,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
if let Expression::Call(call) = expr {
let Some(func_name) = call.function_name_str() else {
return;
};
if let Some(arg_index) = self.get_translatable_param(func_name)
&& let Some(arg) = call.arguments.get(arg_index)
{
self.check_argument(arg, func_name, file, violations);
}
}
}
fn check_argument(
&self,
arg: &Argument,
func_name: &str,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
let Argument::Expression(arg_expr) = arg;
if let Expression::Call(call) = &**arg_expr
&& let Some(name) = call.function_name_str()
{
if matches!(name, "_" | "gettext" | "N_" | "g_dgettext" | "g_dpgettext2") {
return;
}
}
if let Expression::StringLiteral(_) = &**arg_expr {
if let Some(string_value) = arg_expr.extract_string_value() {
if string_value.trim().is_empty() {
return;
}
}
let location = arg_expr.location();
violations.push(self.violation(
&file.path,
location.line,
location.column,
format!(
"User-visible string in {}() should be wrapped with _(\"...\")",
func_name
),
));
}
}
fn get_translatable_param(&self, func_name: &str) -> Option<usize> {
match func_name {
"gtk_label_new" => Some(0),
"gtk_label_set_text" | "gtk_label_set_markup" | "gtk_label_set_label" => Some(1),
"gtk_button_new_with_label" | "gtk_button_new_with_mnemonic" => Some(0),
"gtk_button_set_label" => Some(1),
"gtk_window_set_title" => Some(1),
"gtk_header_bar_set_title" | "gtk_header_bar_set_subtitle" => Some(1),
"gtk_check_button_new_with_label" | "gtk_check_button_new_with_mnemonic" => Some(0),
"gtk_radio_button_new_with_label" | "gtk_radio_button_new_with_mnemonic" => Some(1),
"gtk_entry_set_placeholder_text" | "gtk_entry_set_text" => Some(1),
"gtk_dialog_add_button" => Some(1),
"gtk_message_dialog_new" => Some(4),
"gtk_message_dialog_set_markup" => Some(1),
"adw_message_dialog_new"
| "adw_message_dialog_set_heading"
| "adw_message_dialog_set_body"
| "adw_message_dialog_set_body_use_markup" => Some(1),
"adw_message_dialog_add_response" => Some(2),
"adw_status_page_set_title" | "adw_status_page_set_description" => Some(1),
"adw_toast_new" => Some(0),
"adw_toast_set_title" | "adw_toast_set_button_label" => Some(1),
"adw_preferences_group_set_title" | "adw_preferences_group_set_description" => Some(1),
"adw_preferences_row_set_title" => Some(1),
"adw_action_row_set_title" | "adw_action_row_set_subtitle" => Some(1),
"adw_entry_row_set_title" => Some(1),
"adw_combo_row_set_title" => Some(1),
"adw_expander_row_set_title" | "adw_expander_row_set_subtitle" => Some(1),
"adw_window_title_new" => Some(0),
"adw_window_title_set_title" | "adw_window_title_set_subtitle" => Some(1),
_ => None,
}
}
}