Add script to detect obsolete or undefined locale strings
This commit is contained in:
parent
e7ef615053
commit
4fb5be96b1
6 changed files with 258 additions and 49 deletions
|
|
@ -5,7 +5,6 @@
|
||||||
"db_version_id_does_not_belong_to_the_given_date": "version_id does not belong to the given date",
|
"db_version_id_does_not_belong_to_the_given_date": "version_id does not belong to the given date",
|
||||||
"db_key_incorrect": "The key is probably incorrect",
|
"db_key_incorrect": "The key is probably incorrect",
|
||||||
"db_database_error": "Database error",
|
"db_database_error": "Database error",
|
||||||
"database_path": "Database path",
|
|
||||||
"database_maintenance": "Database maintenance",
|
"database_maintenance": "Database maintenance",
|
||||||
"database_compact": "Compact the database",
|
"database_compact": "Compact the database",
|
||||||
"database_compact_explanation": "Compacting runs VACUUM on the database. This can help reduce its size.",
|
"database_compact_explanation": "Compacting runs VACUUM on the database. This can help reduce its size.",
|
||||||
|
|
@ -33,9 +32,7 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"behaviour": "Behaviour",
|
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"browse": "Browse",
|
|
||||||
"close_tab": "Close tab",
|
"close_tab": "Close tab",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"previous_day": "Previous day",
|
"previous_day": "Previous day",
|
||||||
|
|
@ -45,7 +42,6 @@
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"view_history": "View History",
|
"view_history": "View History",
|
||||||
"export": "Export",
|
|
||||||
"export_accessible_flag": "&Export",
|
"export_accessible_flag": "&Export",
|
||||||
"export_entries": "Export entries",
|
"export_entries": "Export entries",
|
||||||
"export_complete": "Export complete",
|
"export_complete": "Export complete",
|
||||||
|
|
@ -123,20 +119,11 @@
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"manage_tags": "Manage tags",
|
"manage_tags": "Manage tags",
|
||||||
"main_window_manage_tags_accessible_flag": "Manage &Tags",
|
|
||||||
"add_tag_placeholder": "Add a tag and press Enter",
|
"add_tag_placeholder": "Add a tag and press Enter",
|
||||||
"tag_browser_title": "Tag Browser",
|
"tag_browser_title": "Tag Browser",
|
||||||
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
|
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
|
||||||
"tag_name": "Tag name",
|
|
||||||
"tag_color_hex": "Hex colour",
|
|
||||||
"color_hex": "Colour",
|
"color_hex": "Colour",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"pick_color": "Pick colour",
|
|
||||||
"invalid_color_title": "Invalid colour",
|
|
||||||
"invalid_color_message": "Please enter a valid hex colour like #RRGGBB.",
|
|
||||||
"add": "Add",
|
|
||||||
"remove": "Remove",
|
|
||||||
"ok": "OK",
|
|
||||||
"add_a_tag": "Add a tag",
|
"add_a_tag": "Add a tag",
|
||||||
"edit_tag_name": "Edit tag name",
|
"edit_tag_name": "Edit tag name",
|
||||||
"new_tag_name": "New tag name:",
|
"new_tag_name": "New tag name:",
|
||||||
|
|
@ -164,6 +151,7 @@
|
||||||
"bug_report_send_failed": "Could not send bug report.",
|
"bug_report_send_failed": "Could not send bug report.",
|
||||||
"bug_report_sent_ok": "Bug report sent. Thank you!",
|
"bug_report_sent_ok": "Bug report sent. Thank you!",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
|
"reminder": "Reminder",
|
||||||
"set_reminder": "Set reminder prompt",
|
"set_reminder": "Set reminder prompt",
|
||||||
"set_reminder_prompt": "Enter a time",
|
"set_reminder_prompt": "Enter a time",
|
||||||
"reminder_no_text_fallback": "You scheduled a reminder to alert you now!",
|
"reminder_no_text_fallback": "You scheduled a reminder to alert you now!",
|
||||||
|
|
@ -201,7 +189,6 @@
|
||||||
"invalid_activity_title": "Invalid activity",
|
"invalid_activity_title": "Invalid activity",
|
||||||
"invalid_project_message": "The project is invalid",
|
"invalid_project_message": "The project is invalid",
|
||||||
"invalid_project_title": "Invalid project",
|
"invalid_project_title": "Invalid project",
|
||||||
"label_key": "Label",
|
|
||||||
"manage_activities": "Manage activities",
|
"manage_activities": "Manage activities",
|
||||||
"manage_projects": "Manage projects",
|
"manage_projects": "Manage projects",
|
||||||
"manage_projects_activities": "Manage project activities",
|
"manage_projects_activities": "Manage project activities",
|
||||||
|
|
@ -217,8 +204,14 @@
|
||||||
"rename_activity": "Rename activity",
|
"rename_activity": "Rename activity",
|
||||||
"rename_project": "Rename project",
|
"rename_project": "Rename project",
|
||||||
"run_report": "Run report",
|
"run_report": "Run report",
|
||||||
"add_project_label": "Add a project",
|
"add_activity_title": "Add activity",
|
||||||
"add_activity_label": "Add an activity",
|
"add_activity_label": "Add an activity",
|
||||||
|
"rename_activity_label": "Rename activity",
|
||||||
|
"add_project_title": "Add project",
|
||||||
|
"add_project_label": "Add a project",
|
||||||
|
"rename_activity_title": "Rename this activity",
|
||||||
|
"rename_project_label": "Rename project",
|
||||||
|
"rename_project_title": "Rename this project",
|
||||||
"select_activity_message": "Select an activity",
|
"select_activity_message": "Select an activity",
|
||||||
"select_activity_title": "Select activity",
|
"select_activity_title": "Select activity",
|
||||||
"select_project_message": "Select a project",
|
"select_project_message": "Select a project",
|
||||||
|
|
@ -235,7 +228,6 @@
|
||||||
"time_log_total_hours": "Total time spent",
|
"time_log_total_hours": "Total time spent",
|
||||||
"time_log_with_total": "Time log ({hours:.2f}h)",
|
"time_log_with_total": "Time log ({hours:.2f}h)",
|
||||||
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
||||||
"title_key": "title",
|
|
||||||
"update_time_entry": "Update time entry",
|
"update_time_entry": "Update time entry",
|
||||||
"time_report_total": "Total: {hours:.2f} hours",
|
"time_report_total": "Total: {hours:.2f} hours",
|
||||||
"no_report_title": "No report",
|
"no_report_title": "No report",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
"db_version_id_does_not_belong_to_the_given_date": "version_id ne correspond pas à la date indiquée",
|
"db_version_id_does_not_belong_to_the_given_date": "version_id ne correspond pas à la date indiquée",
|
||||||
"db_key_incorrect": "La clé est peut-être incorrecte",
|
"db_key_incorrect": "La clé est peut-être incorrecte",
|
||||||
"db_database_error": "Erreur de base de données",
|
"db_database_error": "Erreur de base de données",
|
||||||
"database_path": "Chemin de la base de données",
|
|
||||||
"database_maintenance": "Maintenance de la base de données",
|
"database_maintenance": "Maintenance de la base de données",
|
||||||
"database_compact": "Compacter la base de données",
|
"database_compact": "Compacter la base de données",
|
||||||
"database_compact_explanation": "La compaction exécute VACUUM sur la base de données. Cela peut aider à réduire sa taille.",
|
"database_compact_explanation": "La compaction exécute VACUUM sur la base de données. Cela peut aider à réduire sa taille.",
|
||||||
|
|
@ -33,9 +32,7 @@
|
||||||
"system": "Système",
|
"system": "Système",
|
||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
"behaviour": "Comportement",
|
|
||||||
"never": "Jamais",
|
"never": "Jamais",
|
||||||
"browse": "Parcourir",
|
|
||||||
"previous": "Précédent",
|
"previous": "Précédent",
|
||||||
"previous_day": "Jour précédent",
|
"previous_day": "Jour précédent",
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
|
|
@ -44,7 +41,6 @@
|
||||||
"show": "Afficher",
|
"show": "Afficher",
|
||||||
"history": "Historique",
|
"history": "Historique",
|
||||||
"view_history": "Afficher l'historique",
|
"view_history": "Afficher l'historique",
|
||||||
"export": "Exporter",
|
|
||||||
"export_accessible_flag": "E&xporter",
|
"export_accessible_flag": "E&xporter",
|
||||||
"export_entries": "Exporter les entrées",
|
"export_entries": "Exporter les entrées",
|
||||||
"export_complete": "Exportation terminée",
|
"export_complete": "Exportation terminée",
|
||||||
|
|
@ -117,16 +113,8 @@
|
||||||
"add_tag_placeholder": "Ajouter une étiquette et appuyez sur Entrée",
|
"add_tag_placeholder": "Ajouter une étiquette et appuyez sur Entrée",
|
||||||
"tag_browser_title": "Navigateur de étiquettes",
|
"tag_browser_title": "Navigateur de étiquettes",
|
||||||
"tag_browser_instructions": "Cliquez sur une étiquette pour l'étendre et voir toutes les pages avec cette étiquette. Cliquez sur une date pour l'ouvrir. Sélectionnez une étiquette pour modifier son nom, changer sa couleur ou la supprimer globalement.",
|
"tag_browser_instructions": "Cliquez sur une étiquette pour l'étendre et voir toutes les pages avec cette étiquette. Cliquez sur une date pour l'ouvrir. Sélectionnez une étiquette pour modifier son nom, changer sa couleur ou la supprimer globalement.",
|
||||||
"tag_name": "Nom de l'étiquette",
|
|
||||||
"tag_color_hex": "Couleur hexadécimale",
|
|
||||||
"color_hex": "Couleur",
|
"color_hex": "Couleur",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"pick_color": "Choisir la couleur",
|
|
||||||
"invalid_color_title": "Couleur invalide",
|
|
||||||
"invalid_color_message": "Veuillez entrer une couleur hexadécimale valide comme #RRGGBB.",
|
|
||||||
"add": "Ajouter",
|
|
||||||
"remove": "Supprimer",
|
|
||||||
"ok": "OK",
|
|
||||||
"add_a_tag": "Ajouter une étiquette",
|
"add_a_tag": "Ajouter une étiquette",
|
||||||
"edit_tag_name": "Modifier le nom de l'étiquette",
|
"edit_tag_name": "Modifier le nom de l'étiquette",
|
||||||
"new_tag_name": "Nouveau nom de l'étiquette :",
|
"new_tag_name": "Nouveau nom de l'étiquette :",
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
"db_issues_reported": "problema/i segnalato/i",
|
"db_issues_reported": "problema/i segnalato/i",
|
||||||
"db_reopen_failed_after_rekey": "Riapertura fallita dopo il cambio chiave",
|
"db_reopen_failed_after_rekey": "Riapertura fallita dopo il cambio chiave",
|
||||||
"db_version_id_does_not_belong_to_the_given_date": "version_id non appartiene alla data indicata",
|
"db_version_id_does_not_belong_to_the_given_date": "version_id non appartiene alla data indicata",
|
||||||
"db_key_incorrect": "La chiave è probabilmente errata",
|
|
||||||
"db_database_error": "Errore del database",
|
"db_database_error": "Errore del database",
|
||||||
"database_path": "Percorso del database",
|
"db_key_incorrect": "La chiave è probabilmente errata",
|
||||||
"database_maintenance": "Manutenzione del database",
|
"database_maintenance": "Manutenzione del database",
|
||||||
"database_compact": "Compatta il database",
|
"database_compact": "Compatta il database",
|
||||||
"database_compact_explanation": "La compattazione esegue VACUUM sul database. Può aiutare a ridurne le dimensioni.",
|
"database_compact_explanation": "La compattazione esegue VACUUM sul database. Può aiutare a ridurne le dimensioni.",
|
||||||
|
|
@ -33,9 +32,7 @@
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"light": "Chiaro",
|
"light": "Chiaro",
|
||||||
"dark": "Scuro",
|
"dark": "Scuro",
|
||||||
"behaviour": "Comportamento",
|
|
||||||
"never": "Mai",
|
"never": "Mai",
|
||||||
"browse": "Sfoglia",
|
|
||||||
"previous": "Precedente",
|
"previous": "Precedente",
|
||||||
"previous_day": "Giorno precedente",
|
"previous_day": "Giorno precedente",
|
||||||
"next": "Successivo",
|
"next": "Successivo",
|
||||||
|
|
@ -44,7 +41,6 @@
|
||||||
"show": "Mostra",
|
"show": "Mostra",
|
||||||
"history": "Cronologia",
|
"history": "Cronologia",
|
||||||
"view_history": "Visualizza cronologia",
|
"view_history": "Visualizza cronologia",
|
||||||
"export": "Esporta",
|
|
||||||
"export_accessible_flag": "&Esporta",
|
"export_accessible_flag": "&Esporta",
|
||||||
"export_entries": "Esporta voci",
|
"export_entries": "Esporta voci",
|
||||||
"export_complete": "Esportazione completata",
|
"export_complete": "Esportazione completata",
|
||||||
|
|
@ -116,16 +112,8 @@
|
||||||
"add_tag_placeholder": "Aggiungi un tag e premi Invio",
|
"add_tag_placeholder": "Aggiungi un tag e premi Invio",
|
||||||
"tag_browser_title": "Browser dei tag",
|
"tag_browser_title": "Browser dei tag",
|
||||||
"tag_browser_instructions": "Fai clic su un tag per espandere e vedere tutte le pagine con quel tag. Fai clic su una data per aprirla. Seleziona un tag per modificarne il nome, cambiarne il colore o eliminarlo globalmente.",
|
"tag_browser_instructions": "Fai clic su un tag per espandere e vedere tutte le pagine con quel tag. Fai clic su una data per aprirla. Seleziona un tag per modificarne il nome, cambiarne il colore o eliminarlo globalmente.",
|
||||||
"tag_name": "Nome del tag",
|
|
||||||
"tag_color_hex": "Colore esadecimale",
|
|
||||||
"color_hex": "Colore",
|
"color_hex": "Colore",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
"pick_color": "Scegli colore",
|
|
||||||
"invalid_color_title": "Colore non valido",
|
|
||||||
"invalid_color_message": "Inserisci un colore esadecimale valido come #RRGGBB.",
|
|
||||||
"add": "Aggiungi",
|
|
||||||
"remove": "Rimuovi",
|
|
||||||
"ok": "OK",
|
|
||||||
"add_a_tag": "Aggiungi un tag",
|
"add_a_tag": "Aggiungi un tag",
|
||||||
"edit_tag_name": "Modifica nome tag",
|
"edit_tag_name": "Modifica nome tag",
|
||||||
"new_tag_name": "Nuovo nome tag:",
|
"new_tag_name": "Nuovo nome tag:",
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ class SettingsDialog(QDialog):
|
||||||
self.idle_spin.setSingleStep(1)
|
self.idle_spin.setSingleStep(1)
|
||||||
self.idle_spin.setAccelerated(True)
|
self.idle_spin.setAccelerated(True)
|
||||||
self.idle_spin.setSuffix(" min")
|
self.idle_spin.setSuffix(" min")
|
||||||
self.idle_spin.setSpecialValueText(strings._("Never"))
|
self.idle_spin.setSpecialValueText(strings._("never"))
|
||||||
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
||||||
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,18 +69,18 @@ class ToolBar(QToolBar):
|
||||||
self.actH3.setCheckable(True)
|
self.actH3.setCheckable(True)
|
||||||
self.actH3.setShortcut("Ctrl+3")
|
self.actH3.setShortcut("Ctrl+3")
|
||||||
self.actH3.triggered.connect(lambda: self.headingRequested.emit(14))
|
self.actH3.triggered.connect(lambda: self.headingRequested.emit(14))
|
||||||
self.actNormal = QAction("N", self)
|
self.actNormal = QAction("P", self)
|
||||||
self.actNormal.setToolTip(strings._("toolbar_normal_paragraph_text"))
|
self.actNormal.setToolTip(strings._("toolbar_normal_paragraph_text"))
|
||||||
self.actNormal.setCheckable(True)
|
self.actNormal.setCheckable(True)
|
||||||
self.actNormal.setShortcut("Ctrl+N")
|
self.actNormal.setShortcut("Ctrl+.")
|
||||||
self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0))
|
self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0))
|
||||||
|
|
||||||
self.actFontSmaller = QAction("N-", self)
|
self.actFontSmaller = QAction("P-", self)
|
||||||
self.actFontSmaller.setToolTip(strings._("toolbar_font_smaller"))
|
self.actFontSmaller.setToolTip(strings._("toolbar_font_smaller"))
|
||||||
self.actFontSmaller.setShortcut("Ctrl+Shift+-")
|
self.actFontSmaller.setShortcut("Ctrl+Shift+-")
|
||||||
self.actFontSmaller.triggered.connect(self.fontSizeSmallerRequested)
|
self.actFontSmaller.triggered.connect(self.fontSizeSmallerRequested)
|
||||||
|
|
||||||
self.actFontLarger = QAction("N+", self)
|
self.actFontLarger = QAction("P+", self)
|
||||||
self.actFontLarger.setToolTip(strings._("toolbar_font_larger"))
|
self.actFontLarger.setToolTip(strings._("toolbar_font_larger"))
|
||||||
self.actFontLarger.setShortcut("Ctrl+Shift+=")
|
self.actFontLarger.setShortcut("Ctrl+Shift+=")
|
||||||
self.actFontLarger.triggered.connect(self.fontSizeLargerRequested)
|
self.actFontLarger.triggered.connect(self.fontSizeLargerRequested)
|
||||||
|
|
@ -167,9 +167,9 @@ class ToolBar(QToolBar):
|
||||||
self._style_letter_button(self.actH1, "H1")
|
self._style_letter_button(self.actH1, "H1")
|
||||||
self._style_letter_button(self.actH2, "H2")
|
self._style_letter_button(self.actH2, "H2")
|
||||||
self._style_letter_button(self.actH3, "H3")
|
self._style_letter_button(self.actH3, "H3")
|
||||||
self._style_letter_button(self.actNormal, "N")
|
self._style_letter_button(self.actNormal, "P")
|
||||||
self._style_letter_button(self.actFontSmaller, "N-")
|
self._style_letter_button(self.actFontSmaller, "P-")
|
||||||
self._style_letter_button(self.actFontLarger, "N+")
|
self._style_letter_button(self.actFontLarger, "P+")
|
||||||
|
|
||||||
# Lists
|
# Lists
|
||||||
self._style_letter_button(self.actBullets, "•")
|
self._style_letter_button(self.actBullets, "•")
|
||||||
|
|
|
||||||
241
find_unused_strings.py
Executable file
241
find_unused_strings.py
Executable file
|
|
@ -0,0 +1,241 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Set
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent / "bouquin"
|
||||||
|
LOCALES_DIR = BASE_DIR / "locales"
|
||||||
|
|
||||||
|
DEFAULT_LOCALE = "en"
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_keys(locale: str = DEFAULT_LOCALE) -> Set[str]:
|
||||||
|
"""Load all keys from the given locale JSON file."""
|
||||||
|
path = LOCALES_DIR / f"{locale}.json"
|
||||||
|
with path.open(encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return set(data.keys())
|
||||||
|
|
||||||
|
|
||||||
|
class KeyParamFinder(ast.NodeVisitor):
|
||||||
|
"""
|
||||||
|
First pass:
|
||||||
|
For each function/method, figure out which parameters are later passed
|
||||||
|
into _(), translated(), or strings._().
|
||||||
|
|
||||||
|
Example: in your _prompt_name, it discovers that title_key and label_key
|
||||||
|
are translation-key parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# func_name -> {"param_positions": {param: arg_index}, "key_param_positions": set[arg_index]}
|
||||||
|
self.func_info: Dict[str, dict] = {}
|
||||||
|
self.current_func_name_stack: list[str] = []
|
||||||
|
self.current_param_positions_stack: list[Dict[str, int]] = []
|
||||||
|
self.current_class_stack: list[str] = []
|
||||||
|
|
||||||
|
# Track when we're inside a class so we can treat "self" specially
|
||||||
|
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||||
|
self.current_class_stack.append(node.name)
|
||||||
|
self.generic_visit(node)
|
||||||
|
self.current_class_stack.pop()
|
||||||
|
|
||||||
|
def _enter_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
||||||
|
funcname = node.name
|
||||||
|
params = [arg.arg for arg in node.args.args]
|
||||||
|
|
||||||
|
# If we're inside a class and there is at least one param,
|
||||||
|
# assume the first one is "self"/"cls" and is implicit at call sites.
|
||||||
|
is_method = bool(self.current_class_stack) and len(params) > 0
|
||||||
|
|
||||||
|
param_positions: Dict[str, int] = {}
|
||||||
|
for i, name in enumerate(params):
|
||||||
|
if is_method and i == 0:
|
||||||
|
# skip self/cls; it doesn't correspond to an explicit arg in calls like self.method(...)
|
||||||
|
continue
|
||||||
|
call_index = i - 1 if is_method else i
|
||||||
|
param_positions[name] = call_index
|
||||||
|
|
||||||
|
self.current_func_name_stack.append(funcname)
|
||||||
|
self.current_param_positions_stack.append(param_positions)
|
||||||
|
|
||||||
|
self.func_info.setdefault(funcname, {
|
||||||
|
"param_positions": param_positions,
|
||||||
|
"key_param_positions": set(),
|
||||||
|
})
|
||||||
|
# If the function name is reused, last definition wins
|
||||||
|
self.func_info[funcname]["param_positions"] = param_positions
|
||||||
|
|
||||||
|
def _exit_function(self) -> None:
|
||||||
|
self.current_func_name_stack.pop()
|
||||||
|
self.current_param_positions_stack.pop()
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||||
|
self._enter_function(node)
|
||||||
|
self.generic_visit(node)
|
||||||
|
self._exit_function()
|
||||||
|
|
||||||
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||||||
|
self._enter_function(node)
|
||||||
|
self.generic_visit(node)
|
||||||
|
self._exit_function()
|
||||||
|
|
||||||
|
def visit_Call(self, node: ast.Call) -> None:
|
||||||
|
# Only care about calls *inside* functions
|
||||||
|
if not self.current_func_name_stack:
|
||||||
|
return self.generic_visit(node)
|
||||||
|
|
||||||
|
func = node.func
|
||||||
|
func_name: str | None = None
|
||||||
|
|
||||||
|
if isinstance(func, ast.Name):
|
||||||
|
func_name = func.id
|
||||||
|
elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
||||||
|
# e.g. strings._(...)
|
||||||
|
func_name = f"{func.value.id}.{func.attr}"
|
||||||
|
|
||||||
|
# Is this a translation call?
|
||||||
|
if func_name in {"_", "translated", "strings._"}:
|
||||||
|
cur_name = self.current_func_name_stack[-1]
|
||||||
|
param_positions = self.current_param_positions_stack[-1]
|
||||||
|
|
||||||
|
# Positional first arg
|
||||||
|
if node.args:
|
||||||
|
first = node.args[0]
|
||||||
|
if isinstance(first, ast.Name):
|
||||||
|
pname = first.id
|
||||||
|
if pname in param_positions:
|
||||||
|
idx = param_positions[pname]
|
||||||
|
self.func_info[cur_name]["key_param_positions"].add(idx)
|
||||||
|
|
||||||
|
# Keyword args, e.g. strings._(key=title_key)
|
||||||
|
for kw in node.keywords or []:
|
||||||
|
if isinstance(kw.value, ast.Name):
|
||||||
|
pname = kw.value.id
|
||||||
|
if pname in param_positions:
|
||||||
|
idx = param_positions[pname]
|
||||||
|
self.func_info[cur_name]["key_param_positions"].add(idx)
|
||||||
|
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
|
||||||
|
class UsedKeyCollector(ast.NodeVisitor):
|
||||||
|
"""
|
||||||
|
Second pass:
|
||||||
|
- Collect string literals passed directly to _()/translated()/strings._()
|
||||||
|
- Collect string literals passed into parameters that we know are
|
||||||
|
"translation-key parameters" of wrapper functions/methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, func_info: Dict[str, dict]) -> None:
|
||||||
|
self.func_info = func_info
|
||||||
|
self.used_keys: Set[str] = set()
|
||||||
|
|
||||||
|
def visit_Call(self, node: ast.Call) -> None:
|
||||||
|
func = node.func
|
||||||
|
|
||||||
|
def full_name(f: ast.expr) -> str | None:
|
||||||
|
if isinstance(f, ast.Name):
|
||||||
|
return f.id
|
||||||
|
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
||||||
|
return f"{f.value.id}.{f.attr}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
func_full = full_name(func)
|
||||||
|
|
||||||
|
# 1) Direct translation calls like _("key") or strings._("key")
|
||||||
|
if func_full in {"_", "translated", "strings._"}:
|
||||||
|
if node.args:
|
||||||
|
first = node.args[0]
|
||||||
|
if isinstance(first, ast.Constant) and isinstance(first.value, str):
|
||||||
|
self.used_keys.add(first.value)
|
||||||
|
for kw in node.keywords or []:
|
||||||
|
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
||||||
|
self.used_keys.add(kw.value.value)
|
||||||
|
|
||||||
|
# 2) Wrapper calls: functions whose params we know are translation-key params
|
||||||
|
called_base_name: str | None = None
|
||||||
|
if isinstance(func, ast.Name):
|
||||||
|
called_base_name = func.id
|
||||||
|
elif isinstance(func, ast.Attribute):
|
||||||
|
called_base_name = func.attr # e.g. self._prompt_name -> "_prompt_name"
|
||||||
|
|
||||||
|
if called_base_name in self.func_info:
|
||||||
|
info = self.func_info[called_base_name]
|
||||||
|
param_positions: Dict[str, int] = info["param_positions"]
|
||||||
|
key_positions: Set[int] = info["key_param_positions"]
|
||||||
|
|
||||||
|
# positional args
|
||||||
|
for idx, arg in enumerate(node.args):
|
||||||
|
if idx in key_positions and isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||||
|
self.used_keys.add(arg.value)
|
||||||
|
|
||||||
|
# keyword args
|
||||||
|
for kw in node.keywords or []:
|
||||||
|
if kw.arg is None:
|
||||||
|
continue # **kwargs, ignore
|
||||||
|
param_name = kw.arg
|
||||||
|
if param_name in param_positions:
|
||||||
|
idx = param_positions[param_name]
|
||||||
|
if idx in key_positions:
|
||||||
|
val = kw.value
|
||||||
|
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
||||||
|
self.used_keys.add(val.value)
|
||||||
|
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_used_keys() -> Set[str]:
|
||||||
|
"""Parse all .py files and collect all translation keys used."""
|
||||||
|
trees: list[ast.AST] = []
|
||||||
|
|
||||||
|
# Read and parse all Python files in this folder
|
||||||
|
for path in BASE_DIR.glob("*.py"):
|
||||||
|
# Optionally skip this script itself
|
||||||
|
if path.name == Path(__file__).name:
|
||||||
|
continue
|
||||||
|
src = path.read_text(encoding="utf-8")
|
||||||
|
tree = ast.parse(src, filename=str(path))
|
||||||
|
trees.append(tree)
|
||||||
|
|
||||||
|
# First pass: find which parameters are translation-key params
|
||||||
|
finder = KeyParamFinder()
|
||||||
|
for tree in trees:
|
||||||
|
finder.visit(tree)
|
||||||
|
|
||||||
|
# Second pass: collect string literals passed to those parameters
|
||||||
|
collector = UsedKeyCollector(finder.func_info)
|
||||||
|
for tree in trees:
|
||||||
|
collector.visit(tree)
|
||||||
|
|
||||||
|
return collector.used_keys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
json_keys = load_json_keys()
|
||||||
|
used_keys = collect_used_keys()
|
||||||
|
|
||||||
|
unused_keys = sorted(json_keys - used_keys)
|
||||||
|
missing_in_json = sorted(used_keys - json_keys)
|
||||||
|
|
||||||
|
print("=== Unused keys in JSON (present in locales but never used in code) ===")
|
||||||
|
if unused_keys:
|
||||||
|
for k in unused_keys:
|
||||||
|
print(" ", k)
|
||||||
|
else:
|
||||||
|
print(" (none)")
|
||||||
|
|
||||||
|
print("\n=== Keys used in code but missing from JSON ===")
|
||||||
|
if missing_in_json:
|
||||||
|
for k in missing_in_json:
|
||||||
|
print(" ", k)
|
||||||
|
else:
|
||||||
|
print(" (none)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue