ai-devsec-gateway 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ai_blocker/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+ # ruff: noqa: F401
3
+ __version__ = "1.2.1"
4
+ APP_VERSION = __version__
5
+
6
+ # Expose everything to maintain backwards compatibility and test stability
7
+ from ai_blocker.block_actions import activate_block, deactivate_block, detect_running_ai_editors, force_close_processes
8
+ from ai_blocker.config import (
9
+ SENSITIVE_CONFIG_KEYS,
10
+ get_config_path,
11
+ get_windows_autostart,
12
+ load_config,
13
+ save_config,
14
+ set_windows_autostart,
15
+ )
16
+ from ai_blocker.constants import (
17
+ BLOCKLIST,
18
+ CATEGORY_ICONS,
19
+ COL_BASE,
20
+ COL_BLUE,
21
+ COL_GREEN,
22
+ COL_MAUVE,
23
+ COL_RED,
24
+ COL_SUBTEXT,
25
+ COL_SURFACE0,
26
+ COL_SURFACE1,
27
+ COL_TEXT,
28
+ COL_YELLOW,
29
+ COMMENT_TAG,
30
+ CURRENT_OS,
31
+ HOSTS_PATH,
32
+ PROCESS_LIST,
33
+ UI_FONT,
34
+ _get_ui_font,
35
+ )
36
+ from ai_blocker.gateway import GatewayHandler
37
+ from ai_blocker.i18n import (
38
+ CATEGORY_TRANSLATIONS,
39
+ LANG_CODE_MAP,
40
+ LANG_DISPLAY_MAP,
41
+ STRINGS,
42
+ detect_system_language,
43
+ load_translations,
44
+ )
45
+ from ai_blocker.system_utils import (
46
+ _get_subprocess_kwargs,
47
+ count_total_domains,
48
+ flush_dns,
49
+ get_hosts_status,
50
+ is_admin,
51
+ relaunch_as_admin,
52
+ )
53
+ from ai_blocker.ui import AIBlockerApp
54
+
55
+ if CURRENT_OS == "Windows":
56
+ from ai_blocker.tray import WindowsTrayIcon
ai_blocker/__main__.py ADDED
@@ -0,0 +1,112 @@
1
+ import argparse
2
+ import ctypes
3
+ import sys
4
+ import tkinter as tk
5
+ from tkinter import messagebox
6
+
7
+ from ai_blocker.constants import CURRENT_OS
8
+ from ai_blocker.i18n import STRINGS, detect_system_language
9
+ from ai_blocker.system_utils import get_hosts_status, is_admin, relaunch_as_admin
10
+ from ai_blocker.ui import AIBlockerApp
11
+
12
+
13
+ def acquire_single_instance_lock():
14
+ if CURRENT_OS == "Windows":
15
+ mutex_name = "Global\\AIBlocker_SingleInstance_Mutex"
16
+ mutex = ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name)
17
+ if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
18
+ return False, None
19
+ return True, mutex
20
+ else:
21
+ try:
22
+ import fcntl
23
+ lock_file = "/tmp/ai_blocker.lock"
24
+ fp = open(lock_file, "w")
25
+ fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
26
+ return True, fp
27
+ except (IOError, OSError, ImportError):
28
+ return False, None
29
+
30
+ def main():
31
+ parser = argparse.ArgumentParser(description="AI Network Blocker & DevSec Gateway CLI")
32
+ parser.add_argument("--block", choices=["work", "personal", "free"], help="Activate blocking for the specified profile")
33
+ parser.add_argument("--unblock", action="store_true", help="Deactivate all AI domain blocks")
34
+ parser.add_argument("--status", action="store_true", help="Show current blocking status and active editors")
35
+
36
+ args, unknown = parser.parse_known_args()
37
+
38
+ # CLI execution path
39
+ if args.block or args.unblock or args.status:
40
+ if args.status:
41
+ is_blocked, count = get_hosts_status()
42
+ print(f"Status: {'PROTECTED (Blocking active)' if is_blocked else 'EXPOSED (No protection)'}")
43
+ print(f"Blocked domains: {count}")
44
+
45
+ from ai_blocker.block_actions import detect_running_ai_editors
46
+ editors = detect_running_ai_editors()
47
+ if editors:
48
+ print(f"Active AI editors detected: {', '.join(editors)}")
49
+ else:
50
+ print("No active AI editors detected.")
51
+ sys.exit(0)
52
+
53
+ # For block/unblock, verify admin privileges
54
+ if not is_admin():
55
+ print("Error: Administrator/root privileges are required for this action.")
56
+ if CURRENT_OS == "Windows":
57
+ print("Requesting Administrator elevation...")
58
+ relaunch_as_admin()
59
+ sys.exit(1)
60
+
61
+ from ai_blocker.block_actions import activate_block, deactivate_block
62
+
63
+ if args.unblock:
64
+ ok, msg = deactivate_block("en")
65
+ print(msg.strip())
66
+ sys.exit(0 if ok else 1)
67
+
68
+ if args.block:
69
+ from ai_blocker.constants import BLOCKLIST
70
+ if args.block == "work":
71
+ cats = list(BLOCKLIST.keys())
72
+ elif args.block == "personal":
73
+ cats = [c for c in BLOCKLIST.keys() if "copilot" in c.lower()]
74
+ else: # free
75
+ cats = []
76
+
77
+ if not cats:
78
+ ok, msg = deactivate_block("en")
79
+ else:
80
+ ok, msg = activate_block("en", cats)
81
+ print(msg.strip())
82
+ sys.exit(0 if ok else 1)
83
+
84
+ # GUI execution path
85
+ ok, lock_ref = acquire_single_instance_lock()
86
+ if not ok:
87
+ print("AI Network Blocker is already running.")
88
+ sys.exit(0)
89
+
90
+ if not is_admin():
91
+ relaunch_as_admin()
92
+
93
+ detected_lang = detect_system_language()
94
+ s = STRINGS[detected_lang]
95
+
96
+ root_temp = tk.Tk()
97
+ root_temp.withdraw()
98
+ messagebox.showerror(
99
+ s["admin_required_title"],
100
+ s["admin_required_msg"]
101
+ )
102
+ root_temp.destroy()
103
+ sys.exit(1)
104
+
105
+ root = tk.Tk()
106
+ AIBlockerApp(root)
107
+ if "--minimized" in sys.argv and CURRENT_OS == "Windows":
108
+ root.withdraw()
109
+ root.mainloop()
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,137 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import subprocess
4
+
5
+ from ai_blocker.constants import BLOCKLIST, COMMENT_TAG, CURRENT_OS, HOSTS_PATH, PROCESS_LIST
6
+ from ai_blocker.i18n import STRINGS
7
+ from ai_blocker.system_utils import _get_subprocess_kwargs, flush_dns
8
+
9
+
10
+ def force_close_processes():
11
+ closed = []
12
+ kwargs = _get_subprocess_kwargs()
13
+
14
+ try:
15
+ if CURRENT_OS == "Windows":
16
+ args = ["taskkill", "/F"]
17
+ for proc in PROCESS_LIST:
18
+ args.extend(["/IM", proc])
19
+ result = subprocess.run(args, capture_output=True, text=True, **kwargs)
20
+ out_lower = result.stdout.lower()
21
+ for proc in PROCESS_LIST:
22
+ if f'"{proc.lower()}"' in out_lower or f'{proc.lower()}' in out_lower:
23
+ closed.append(proc.replace(".exe", ""))
24
+ else:
25
+ active = detect_running_ai_editors()
26
+ if active:
27
+ args = ["killall"] + active
28
+ subprocess.run(args, capture_output=True, text=True, **kwargs)
29
+ closed = active
30
+ except Exception:
31
+ pass
32
+ return closed
33
+
34
+ def detect_running_ai_editors():
35
+ running = []
36
+ kwargs = _get_subprocess_kwargs()
37
+
38
+ try:
39
+ if CURRENT_OS == "Windows":
40
+ result = subprocess.run(["tasklist", "/NH"], capture_output=True, text=True, **kwargs)
41
+ out_lower = result.stdout.lower()
42
+ for proc in PROCESS_LIST:
43
+ if proc.lower() in out_lower:
44
+ running.append(proc.replace(".exe", ""))
45
+ else:
46
+ result = subprocess.run(["ps", "-A", "-o", "comm="], capture_output=True, text=True, **kwargs)
47
+ out_lines = result.stdout.splitlines()
48
+ active = set()
49
+ for line in out_lines:
50
+ active.add(os.path.basename(line.strip()).lower())
51
+
52
+ for proc in PROCESS_LIST:
53
+ if proc.lower() in active:
54
+ running.append(proc)
55
+ except Exception:
56
+ pass
57
+ return running
58
+
59
+ def activate_block(lang, categories_to_block=None):
60
+ if categories_to_block is None:
61
+ categories_to_block = list(BLOCKLIST.keys())
62
+
63
+ closed_list = force_close_processes()
64
+ s = STRINGS[lang]
65
+
66
+ try:
67
+ existing_lines = []
68
+ if os.path.exists(HOSTS_PATH):
69
+ with open(HOSTS_PATH, "r", encoding="utf-8") as f:
70
+ existing_lines = f.readlines()
71
+
72
+ cleaned_lines = [l for l in existing_lines if COMMENT_TAG not in l]
73
+
74
+ if cleaned_lines and not cleaned_lines[-1].endswith("\n"):
75
+ cleaned_lines[-1] += "\n"
76
+
77
+ new_entries = []
78
+ added_count = 0
79
+
80
+ domains_to_block = []
81
+ for cat in categories_to_block:
82
+ if cat in BLOCKLIST:
83
+ for domain in BLOCKLIST[cat]:
84
+ if domain not in domains_to_block:
85
+ domains_to_block.append(domain)
86
+
87
+ for domain in domains_to_block:
88
+ entry = f"127.0.0.1 {domain} {COMMENT_TAG}\n"
89
+ new_entries.append(entry)
90
+ added_count += 1
91
+
92
+ with open(HOSTS_PATH, "w", encoding="utf-8") as f:
93
+ f.writelines(cleaned_lines + new_entries)
94
+
95
+ flush_dns()
96
+
97
+ if closed_list:
98
+ process_details = f"{s['closed_processes_prefix']}{', '.join(closed_list)}"
99
+ else:
100
+ process_details = s["no_processes_detected"]
101
+
102
+ msg = s["block_success_msg"].format(
103
+ added_count=added_count,
104
+ process_details=process_details
105
+ )
106
+ return True, msg
107
+
108
+ except PermissionError:
109
+ return False, s["hosts_write_error_msg"]
110
+ except Exception as e:
111
+ return False, s["unexpected_error_msg"].format(error=str(e))
112
+
113
+ def deactivate_block(lang):
114
+ s = STRINGS[lang]
115
+ try:
116
+ if not os.path.exists(HOSTS_PATH):
117
+ return True, s["unblock_success_msg"].format(removed=0)
118
+
119
+ with open(HOSTS_PATH, "r", encoding="utf-8") as f:
120
+ lines = f.readlines()
121
+
122
+ original_count = len(lines)
123
+ cleaned = [l for l in lines if COMMENT_TAG not in l]
124
+ removed = original_count - len(cleaned)
125
+
126
+ with open(HOSTS_PATH, "w", encoding="utf-8") as f:
127
+ f.writelines(cleaned)
128
+
129
+ flush_dns()
130
+
131
+ msg = s["unblock_success_msg"].format(removed=removed)
132
+ return True, msg
133
+
134
+ except PermissionError:
135
+ return False, s["hosts_write_error_msg"]
136
+ except Exception as e:
137
+ return False, s["unexpected_error_msg"].format(error=str(e))
ai_blocker/config.py ADDED
@@ -0,0 +1,85 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ from ai_blocker.constants import CURRENT_OS
7
+
8
+ SENSITIVE_CONFIG_KEYS = {"openai_key"}
9
+
10
+ def get_config_path():
11
+ if CURRENT_OS == "Windows":
12
+ base_dir = os.environ.get("APPDATA", os.path.expanduser("~"))
13
+ else:
14
+ base_dir = os.path.expanduser("~/.config")
15
+ app_dir = os.path.join(base_dir, "AI-Blocker")
16
+ try:
17
+ os.makedirs(app_dir, exist_ok=True)
18
+ except Exception:
19
+ pass
20
+ return os.path.join(app_dir, "config.json")
21
+
22
+ def load_config():
23
+ path = get_config_path()
24
+ if os.path.exists(path):
25
+ try:
26
+ with open(path, "r", encoding="utf-8") as f:
27
+ data = json.load(f)
28
+ for key in SENSITIVE_CONFIG_KEYS:
29
+ data.pop(key, None)
30
+ return data
31
+ except Exception:
32
+ pass
33
+ return {}
34
+
35
+ def save_config(config_data):
36
+ path = get_config_path()
37
+ try:
38
+ safe_config = dict(config_data)
39
+ for key in SENSITIVE_CONFIG_KEYS:
40
+ safe_config.pop(key, None)
41
+ with open(path, "w", encoding="utf-8") as f:
42
+ json.dump(safe_config, f, indent=4, ensure_ascii=False)
43
+ except Exception:
44
+ pass
45
+
46
+ def set_windows_autostart(enabled=True):
47
+ if CURRENT_OS != "Windows":
48
+ return False
49
+ import winreg
50
+ key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
51
+ try:
52
+ # Determine script/executable command line
53
+ if getattr(sys, 'frozen', False):
54
+ cmd = f'"{sys.executable}" --minimized'
55
+ else:
56
+ # When modularized, the root file is still ai_blocker.py
57
+ # But let's check: if we are running as module, we need to run main file
58
+ root_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ai_blocker.py"))
59
+ cmd = f'"{sys.executable}" "{root_file}" --minimized'
60
+
61
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE)
62
+ if enabled:
63
+ winreg.SetValueEx(key, "AIBlocker", 0, winreg.REG_SZ, cmd)
64
+ else:
65
+ try:
66
+ winreg.DeleteValue(key, "AIBlocker")
67
+ except FileNotFoundError:
68
+ pass
69
+ winreg.CloseKey(key)
70
+ return True
71
+ except Exception:
72
+ return False
73
+
74
+ def get_windows_autostart():
75
+ if CURRENT_OS != "Windows":
76
+ return False
77
+ import winreg
78
+ key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
79
+ try:
80
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_READ)
81
+ val, _ = winreg.QueryValueEx(key, "AIBlocker")
82
+ winreg.CloseKey(key)
83
+ return True
84
+ except Exception:
85
+ return False
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import platform
4
+
5
+ CURRENT_OS = platform.system() # 'Windows', 'Linux', 'Darwin'
6
+
7
+ BLOCKLIST = {
8
+ "OpenAI": [
9
+ "api.openai.com", "chatgpt.com", "chat.openai.com",
10
+ "platform.openai.com", "openai.com", "auth.openai.com",
11
+ "oaistatic.com", "oaiusercontent.com", "labs.openai.com",
12
+ ],
13
+ "Anthropic": [
14
+ "api.anthropic.com", "claude.ai", "anthropic.com",
15
+ "claudeusercontent.com",
16
+ ],
17
+ "GitHub Copilot": [
18
+ "api.githubcopilot.com", "copilot.github.com",
19
+ "githubcopilot.com", "api.individual.githubcopilot.com",
20
+ ],
21
+ "Google AI": [
22
+ "generativelanguage.googleapis.com", "aistudio.google.com",
23
+ "gemini.google.com", "ai.google.dev",
24
+ ],
25
+ "Meta AI": [
26
+ "meta.ai", "ai.meta.com",
27
+ ],
28
+ "Mistral AI": [
29
+ "api.mistral.ai", "mistral.ai",
30
+ ],
31
+ "Microsoft Copilot": [
32
+ "copilot.microsoft.com", "bing.com", "edgeservices.bing.com",
33
+ ],
34
+ "DeepSeek": [
35
+ "deepseek.com", "api.deepseek.com",
36
+ ],
37
+ "xAI": [
38
+ "api.x.ai", "grok.x.ai", "x.ai",
39
+ ],
40
+ "Otros": [
41
+ "api.perplexity.ai", "perplexity.ai",
42
+ "app.wordware.ai",
43
+ ],
44
+ }
45
+
46
+ CATEGORY_ICONS = {
47
+ "OpenAI": "🟢", "Anthropic": "🟠", "GitHub Copilot": "🐙",
48
+ "Google AI": "🔵", "Meta AI": "🔷", "Mistral AI": "🌊",
49
+ "Microsoft Copilot": "🟦", "DeepSeek": "🔮", "xAI": "🤖", "Otros": "📦",
50
+ }
51
+
52
+ if CURRENT_OS == "Windows":
53
+ PROCESS_LIST = [
54
+ "Code.exe", "Cursor.exe", "Windsurf.exe", "Claude.exe",
55
+ "Trae.exe", "Cline.exe", "Roo.exe", "Augment.exe",
56
+ ]
57
+ else:
58
+ PROCESS_LIST = [
59
+ "code", "cursor", "windsurf", "claude",
60
+ "trae", "cline", "roo", "augment",
61
+ ]
62
+
63
+ COMMENT_TAG = "# AI-Block"
64
+
65
+ if CURRENT_OS == "Windows":
66
+ HOSTS_PATH = os.path.join(
67
+ os.environ.get("SystemRoot", r"C:\Windows"),
68
+ r"System32\drivers\etc\hosts",
69
+ )
70
+ else:
71
+ HOSTS_PATH = "/etc/hosts"
72
+
73
+ # Catppuccin Mocha colors
74
+ COL_BASE = "#1E1E2E"
75
+ COL_SURFACE0 = "#313244"
76
+ COL_SURFACE1 = "#45475A"
77
+ COL_TEXT = "#CDD6F4"
78
+ COL_SUBTEXT = "#A6ADC8"
79
+ COL_RED = "#F38BA8"
80
+ COL_GREEN = "#A6E3A1"
81
+ COL_YELLOW = "#F9E2AF"
82
+ COL_BLUE = "#89B4FA"
83
+ COL_MAUVE = "#CBA6F7"
84
+
85
+ def _get_ui_font():
86
+ if CURRENT_OS == "Windows":
87
+ return "Segoe UI"
88
+ elif CURRENT_OS == "Darwin":
89
+ return "Helvetica Neue"
90
+ else:
91
+ return "DejaVu Sans"
92
+
93
+ UI_FONT = _get_ui_font()
ai_blocker/gateway.py ADDED
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ import urllib.error
3
+ import urllib.request
4
+ from http.server import BaseHTTPRequestHandler
5
+
6
+
7
+ class GatewayHandler(BaseHTTPRequestHandler):
8
+ def do_GET(self):
9
+ self._proxy_request("GET")
10
+
11
+ def do_POST(self):
12
+ self._proxy_request("POST")
13
+
14
+ def do_OPTIONS(self):
15
+ self._proxy_request("OPTIONS")
16
+
17
+ def _proxy_request(self, method):
18
+ target = self.server.target_url.rstrip("/") + self.path
19
+ headers = {}
20
+ for k, v in self.headers.items():
21
+ if k.lower() not in ['host', 'accept-encoding']:
22
+ headers[k] = v
23
+
24
+ data = None
25
+ if method in ['POST', 'PUT', 'PATCH']:
26
+ content_length = int(self.headers.get('Content-Length', 0))
27
+ if content_length > 0:
28
+ data = self.rfile.read(content_length)
29
+
30
+ req = urllib.request.Request(target, data=data, headers=headers, method=method)
31
+ try:
32
+ with urllib.request.urlopen(req, timeout=30) as response:
33
+ self.send_response(response.status)
34
+ for k, v in response.headers.items():
35
+ if k.lower() not in ['transfer-encoding']:
36
+ self.send_header(k, v)
37
+ self.end_headers()
38
+
39
+ while True:
40
+ chunk = response.read(1024)
41
+ if not chunk:
42
+ break
43
+ self.wfile.write(chunk)
44
+ self.wfile.flush()
45
+ except urllib.error.HTTPError as e:
46
+ self.send_response(e.code)
47
+ for k, v in e.headers.items():
48
+ if k.lower() not in ['transfer-encoding']:
49
+ self.send_header(k, v)
50
+ self.end_headers()
51
+ self.wfile.write(e.read())
52
+ except Exception as e:
53
+ self.send_response(502)
54
+ self.end_headers()
55
+ self.wfile.write(f"Gateway Error: {e}".encode())
ai_blocker/i18n.py ADDED
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ import locale
4
+ import os
5
+ import sys
6
+
7
+ from ai_blocker.constants import CURRENT_OS
8
+
9
+ if CURRENT_OS == "Windows":
10
+ import ctypes
11
+
12
+ LANG_DISPLAY_MAP = {
13
+ "English": "en",
14
+ "Español": "es",
15
+ "Português": "pt",
16
+ "Français": "fr",
17
+ "Deutsch": "de",
18
+ "Italiano": "it",
19
+ "Русский": "ru",
20
+ "中文 (简体)": "zh",
21
+ "日本語": "ja",
22
+ "한국어": "ko"
23
+ }
24
+ LANG_CODE_MAP = {v: k for k, v in LANG_DISPLAY_MAP.items()}
25
+
26
+ CATEGORY_TRANSLATIONS = {}
27
+ STRINGS = {}
28
+
29
+ def load_translations():
30
+ global CATEGORY_TRANSLATIONS, STRINGS
31
+ try:
32
+ # Load relative to package or executable directory
33
+ if getattr(sys, 'frozen', False):
34
+ base_dir = os.path.dirname(sys.executable)
35
+ else:
36
+ base_dir = os.path.dirname(os.path.abspath(__file__))
37
+
38
+ trans_path = os.path.join(base_dir, "translations.json")
39
+
40
+ # Fallback to parent directory if in development subfolder
41
+ if not os.path.exists(trans_path):
42
+ trans_path = os.path.join(os.path.dirname(base_dir), "translations.json")
43
+
44
+ # Try PyInstaller temporary folder fallback (if bundled as data file)
45
+ if not os.path.exists(trans_path) and hasattr(sys, '_MEIPASS'):
46
+ trans_path = os.path.join(sys._MEIPASS, "translations.json")
47
+
48
+ # Fallback if still not found
49
+ if not os.path.exists(trans_path):
50
+ trans_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translations.json")
51
+
52
+ with open(trans_path, "r", encoding="utf-8") as f:
53
+ data = json.load(f)
54
+ CATEGORY_TRANSLATIONS = data["category_translations"]
55
+ STRINGS = data["strings"]
56
+ except Exception as e:
57
+ print(f"Warning: Failed to load translations.json: {e}")
58
+ # Minimal hardcoded fallback
59
+ CATEGORY_TRANSLATIONS = {"en": {"OpenAI": "OpenAI", "Anthropic": "Anthropic", "GitHub Copilot": "GitHub Copilot", "Google AI": "Google AI", "Meta AI": "Meta AI", "Mistral AI": "Mistral AI", "Microsoft Copilot": "Microsoft Copilot", "DeepSeek": "DeepSeek", "xAI": "xAI", "Otros": "Others"}}
60
+ STRINGS = {"en": {
61
+ "protected_title": "PROTECTED — Blocking active",
62
+ "protected_desc": "{count} domains redirected to 127.0.0.1",
63
+ "exposed_title": "EXPOSED — No protection",
64
+ "exposed_desc": "Your network traffic to AI is open",
65
+ "btn_block": "🔒 BLOCK AI",
66
+ "btn_unblock": "🔓 UNBLOCK AI",
67
+ "busy_text": "⏳ Processing...",
68
+ "categories_title": "Blocked Categories",
69
+ "domains_label": "{total} domains",
70
+ "running_warning": "⚠ Detected: {editors}",
71
+ "hosts_write_error_title": "Permission Error",
72
+ "hosts_write_error_msg": "Could not write to the hosts file.\nPlease run the application as Administrator.",
73
+ "unexpected_error_title": "Unexpected Error",
74
+ "unexpected_error_msg": "An error occurred:\n{error}",
75
+ "block_success_title": "AI Blocking Active",
76
+ "block_success_msg": "Block successfully activated!\n\n✓ {added_count} domains blocked in hosts file.\n{process_details}\n✓ DNS cache flushed.",
77
+ "unblock_success_title": "AI Blocking Disabled",
78
+ "unblock_success_msg": "Block successfully deactivated!\n\n✓ {removed} entries removed from hosts file.\n✓ DNS cache flushed.\n\nAll AI tools can access the network again.",
79
+ "closed_processes_prefix": "✓ Closed processes: ",
80
+ "no_processes_detected": "• No open AI editors detected.",
81
+ "admin_required_title": "Access Denied",
82
+ "admin_required_msg_windows": "Administrator privileges are required.\n\nRight-click → 'Run as administrator'.",
83
+ "admin_required_msg_unix": "Root privileges are required.\n\nRun with: sudo python3 ai_blocker.py",
84
+ "profile_work": "Work",
85
+ "profile_personal": "Personal",
86
+ "profile_free": "Free",
87
+ "profile_custom": "Custom"
88
+ }}
89
+
90
+ # Resolve platform specific strings
91
+ for lang in STRINGS:
92
+ if CURRENT_OS == "Windows":
93
+ STRINGS[lang]["admin_required_msg"] = STRINGS[lang].get("admin_required_msg_windows", "")
94
+ else:
95
+ STRINGS[lang]["admin_required_msg"] = STRINGS[lang].get("admin_required_msg_unix", "")
96
+
97
+ def detect_system_language():
98
+ # 1. Environment variables
99
+ for env_var in ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
100
+ val = os.environ.get(env_var)
101
+ if val:
102
+ code = val.split('_')[0].split('.')[0].lower()
103
+ if code in STRINGS:
104
+ return code
105
+
106
+ # 2. locale.getlocale()
107
+ try:
108
+ if hasattr(locale, 'getlocale'):
109
+ lang, _ = locale.getlocale()
110
+ if lang:
111
+ code = lang.split('_')[0].lower()
112
+ if code in STRINGS:
113
+ return code
114
+ except Exception:
115
+ pass
116
+
117
+ # Windows LCID query
118
+ if CURRENT_OS == "Windows":
119
+ try:
120
+ lcid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
121
+ lcid_map = {
122
+ 1034: "es", 2058: "es", 3082: "es", 4106: "es", 5130: "es", 6154: "es",
123
+ 1046: "pt", 2070: "pt",
124
+ 1036: "fr", 2060: "fr", 3084: "fr",
125
+ 1031: "de", 2055: "de",
126
+ 1040: "it",
127
+ 1049: "ru",
128
+ 2052: "zh", 1028: "zh", 3076: "zh",
129
+ 1041: "ja",
130
+ 1042: "ko",
131
+ }
132
+ for k, v in lcid_map.items():
133
+ if lcid == k or (lcid & 0x3FF) == (k & 0x3FF):
134
+ return v
135
+ except Exception:
136
+ pass
137
+
138
+ return "en"
139
+
140
+ # Run initial load on import
141
+ load_translations()