ezshell 0.2.0__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.
src/i18n.py ADDED
@@ -0,0 +1,422 @@
1
+ """
2
+ ezShell Internationalization (i18n) — Bilingual FR/EN Support
3
+ ==============================================================
4
+
5
+ Provides all user-facing strings in both English and French.
6
+ The active language is resolved from the environment ($LANG, $LANGUAGE, $LC_ALL),
7
+ falling back to English when no French locale is detected.
8
+
9
+ ---
10
+
11
+ Module d'internationalisation bilingue d'ezShell (FR / EN)
12
+ ===========================================================
13
+
14
+ Fournit toutes les chaînes visibles par l'utilisateur en anglais et en français.
15
+ La langue active est détectée depuis les variables d'environnement ($LANG, etc.),
16
+ avec repli sur l'anglais si aucune locale française n'est trouvée.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import locale
21
+ import os
22
+ from enum import Enum
23
+ from typing import Optional
24
+
25
+ from src import __version__
26
+
27
+
28
+ class Lang(str, Enum):
29
+ """Supported interface languages.
30
+
31
+ Langues d'interface prises en charge.
32
+ """
33
+
34
+ EN = "en"
35
+ FR = "fr"
36
+
37
+
38
+ def detect_language() -> Lang:
39
+ """Auto-detects the system language and returns the matching Lang variant.
40
+
41
+ Checks, in order:
42
+ 1. EZSHELL_LANG environment variable (allows explicit override)
43
+ 2. $LANGUAGE, $LANG, $LC_ALL, $LC_MESSAGES environment variables
44
+ 3. Python locale.getlocale()
45
+
46
+ Returns ``Lang.FR`` if a French locale is found, ``Lang.EN`` otherwise.
47
+
48
+ Returns:
49
+ Lang: The detected interface language.
50
+
51
+ ---
52
+
53
+ Détecte automatiquement la langue système et retourne la variante Lang correspondante.
54
+
55
+ Vérifie, dans l'ordre :
56
+ 1. Variable d'environnement EZSHELL_LANG (remplace tout le reste si présente)
57
+ 2. $LANGUAGE, $LANG, $LC_ALL, $LC_MESSAGES
58
+ 3. Python locale.getlocale()
59
+
60
+ Retourne ``Lang.FR`` si une locale française est détectée, ``Lang.EN`` sinon.
61
+ """
62
+ # 1. Explicit override via EZSHELL_LANG=fr or EZSHELL_LANG=en
63
+ override = os.environ.get("EZSHELL_LANG", "").strip().lower()
64
+ if override.startswith("fr"):
65
+ return Lang.FR
66
+ if override.startswith("en"):
67
+ return Lang.EN
68
+
69
+ # 2. Standard locale environment variables
70
+ for var in ("LANGUAGE", "LANG", "LC_ALL", "LC_MESSAGES"):
71
+ val = os.environ.get(var, "").lower()
72
+ if val.startswith("fr"):
73
+ return Lang.FR
74
+
75
+ # 3. Python locale fallback
76
+ try:
77
+ lang_code, _ = locale.getlocale()
78
+ if lang_code and lang_code.lower().startswith("fr"):
79
+ return Lang.FR
80
+ except Exception:
81
+ pass
82
+
83
+ return Lang.EN
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Bilingual string catalogue
88
+ # All user-facing text lives here — never hardcode UI strings in other modules.
89
+ # ---------------------------------------------------------------------------
90
+
91
+ # Each key maps to a dict with "en" and "fr" entries.
92
+ _STRINGS: dict[str, dict[str, str]] = {
93
+ # --- Generic UI ---
94
+ "app_title": {
95
+ "en": "ezShell",
96
+ "fr": "ezShell",
97
+ },
98
+ "esc_to_close": {
99
+ "en": "to close",
100
+ "fr": "pour fermer",
101
+ },
102
+ "thinking": {
103
+ "en": "› ezShell thinking...",
104
+ "fr": "› ezShell analyse...",
105
+ },
106
+
107
+ # --- Diagnostic Card ---
108
+ "section_cause": {
109
+ "en": "CAUSE",
110
+ "fr": "CAUSE",
111
+ },
112
+ "section_explanation": {
113
+ "en": "EXPLANATION",
114
+ "fr": "EXPLICATION",
115
+ },
116
+ "section_fix": {
117
+ "en": "SUGGESTED COMMAND",
118
+ "fr": "COMMANDE RECOMMANDEE",
119
+ },
120
+ "section_ask": {
121
+ "en": "ASK ANOTHER QUESTION",
122
+ "fr": "POSER UNE AUTRE QUESTION",
123
+ },
124
+ "risk_low": {
125
+ "en": "LOW RISK",
126
+ "fr": "RISQUE FAIBLE",
127
+ },
128
+ "risk_medium": {
129
+ "en": "MEDIUM RISK",
130
+ "fr": "RISQUE MODERE",
131
+ },
132
+ "risk_high": {
133
+ "en": "HIGH RISK",
134
+ "fr": "RISQUE ELEVE",
135
+ },
136
+ "risk_reason_default": {
137
+ "en": "Only affects your local environment.",
138
+ "fr": "N'affecte que votre environnement local.",
139
+ },
140
+
141
+ # --- Action Pills ---
142
+ "action_run": {
143
+ "en": "run",
144
+ "fr": "executer",
145
+ },
146
+ "action_copy": {
147
+ "en": "copy",
148
+ "fr": "copier",
149
+ },
150
+ "action_edit": {
151
+ "en": "edit",
152
+ "fr": "modifier",
153
+ },
154
+ "action_close": {
155
+ "en": "close",
156
+ "fr": "fermer",
157
+ },
158
+
159
+ # --- Input Placeholder ---
160
+ "input_placeholder": {
161
+ "en": "Type a question or error to diagnose... (Enter to ask)",
162
+ "fr": "Tapez une question ou une erreur à diagnostiquer... (Entrée pour envoyer)",
163
+ },
164
+
165
+ # --- Command Execution ---
166
+ "cmd_success": {
167
+ "en": "› Command finished (exit 0)",
168
+ "fr": "› Commande terminée (code 0)",
169
+ },
170
+ "cmd_no_output": {
171
+ "en": "(No output)",
172
+ "fr": "(Aucune sortie)",
173
+ },
174
+
175
+ # --- Welcome Banner ---
176
+ "welcome_title": {
177
+ "en": f"ezShell v{__version__}",
178
+ "fr": f"ezShell v{__version__}",
179
+ },
180
+ "welcome_subtitle": {
181
+ "en": "Intelligent Terminal Assistant\nPowered by Groq AI",
182
+ "fr": "Assistant de terminal intelligent\nAlimenté par Groq AI",
183
+ },
184
+ "welcome_body": {
185
+ "en": "Welcome to your new terminal superpower!\nezShell gives you instant AI diagnostics, command\ngeneration, and an interactive desktop assistant\nwindow right from your CLI.",
186
+ "fr": "Bienvenue dans votre nouveau superpouvoir terminal !\nezShell vous offre des diagnostics IA instantanés,\nla génération de commandes et une fenêtre assistant\ndirectement depuis votre ligne de commande.",
187
+ },
188
+ "welcome_how_to_use": {
189
+ "en": "› How to use:",
190
+ "fr": "› Comment utiliser :",
191
+ },
192
+ "welcome_cmd_desktop": {
193
+ "en": "Desktop Assistant window",
194
+ "fr": "Fenêtre assistant bureau",
195
+ },
196
+ "welcome_cmd_tui": {
197
+ "en": "In-terminal TUI popup",
198
+ "fr": "Popup TUI dans le terminal",
199
+ },
200
+ "welcome_cmd_setkey": {
201
+ "en": "Configure your Groq API key",
202
+ "fr": "Configurer votre clé API Groq",
203
+ },
204
+ "welcome_cmd_single": {
205
+ "en": "Single command with AI assist",
206
+ "fr": "Commande unique avec assistance IA",
207
+ },
208
+ "welcome_cmd_help": {
209
+ "en": "List all options",
210
+ "fr": "Lister toutes les options",
211
+ },
212
+ "welcome_footer": {
213
+ "en": "Press Esc or Ctrl+C to close popup windows.",
214
+ "fr": "Appuyez sur Echap ou Ctrl+C pour fermer les fenêtres popup.",
215
+ },
216
+
217
+ # --- Errors ---
218
+ "error_no_api_key": {
219
+ "en": "GROQ_API_KEY is missing. Run 'ezsh --set-key <key>' to configure.",
220
+ "fr": "GROQ_API_KEY manquante. Lancez 'ezsh --set-key <cle>' pour configurer.",
221
+ },
222
+ "error_invalid_key": {
223
+ "en": "Groq rejected the API key (invalid or revoked). Create a new one at "
224
+ "console.groq.com/keys, then run 'ezsh --set-key <key>'.",
225
+ "fr": "Groq refuse la clé API (invalide ou révoquée). Créez-en une sur "
226
+ "console.groq.com/keys, puis lancez 'ezsh --set-key <clé>'.",
227
+ },
228
+ "error_api_generic": {
229
+ "en": "Error querying Groq AI: ",
230
+ "fr": "Erreur lors de la requête Groq AI : ",
231
+ },
232
+
233
+ # --- GUI Top-bar context ---
234
+ "btn_clear": {
235
+ "en": "Clear",
236
+ "fr": "Effacer",
237
+ },
238
+ "btn_send": {
239
+ "en": "Send ›",
240
+ "fr": "Envoyer ›",
241
+ },
242
+
243
+ # --- GUI Welcome card ---
244
+ "gui_welcome_title": {
245
+ "en": "ezShell AI Assistant",
246
+ "fr": "Assistant IA ezShell",
247
+ },
248
+ "gui_welcome_desc": {
249
+ "en": "Ask questions, diagnose errors, or describe any task you need executed.",
250
+ "fr": "Posez des questions, diagnostiquez des erreurs ou décrivez une tâche à exécuter.",
251
+ },
252
+
253
+ # --- Quick suggestion chips (GUI) ---
254
+ "suggestion_ports": {
255
+ "en": "List listening ports on system",
256
+ "fr": "Lister les ports en écoute",
257
+ },
258
+ "suggestion_disk": {
259
+ "en": "Show disk space for current folder",
260
+ "fr": "Afficher l'espace disque du dossier courant",
261
+ },
262
+ "suggestion_docker": {
263
+ "en": "Clean up unused docker containers",
264
+ "fr": "Nettoyer les conteneurs Docker inutilisés",
265
+ },
266
+ "suggestion_git": {
267
+ "en": "Undo last git commit keeping changes",
268
+ "fr": "Annuler le dernier commit git en gardant les modifications",
269
+ },
270
+
271
+ # --- TUI context bar ---
272
+ "tui_move_hint": {
273
+ "en": "[Drag / Alt+Arrows] Move • [Esc] Close",
274
+ "fr": "[Drag / Alt+Flèches] Déplacer • [Echap] Fermer",
275
+ },
276
+ "tui_no_key_msg": {
277
+ "en": "ezShell requires a Groq API key to activate AI diagnostics & chat.\nGet a free key at https://console.groq.com and paste it below:",
278
+ "fr": "ezShell nécessite une clé API Groq pour activer les diagnostics IA.\nObtenez une clé gratuite sur https://console.groq.com et collez-la ci-dessous :",
279
+ },
280
+
281
+ # --- Safety guard reasons (user-facing) ---
282
+ "safe_reason_default": {
283
+ "en": "Command is considered safe",
284
+ "fr": "La commande est considérée sans danger",
285
+ },
286
+
287
+ # --- Launcher & History Detection ---
288
+ "last_cmd_detected": {
289
+ "en": "Last command detected in terminal:",
290
+ "fr": "Dernière commande détectée dans le terminal :",
291
+ },
292
+ "btn_diagnose_error": {
293
+ "en": "Diagnose this error",
294
+ "fr": "Diagnostiquer cette erreur",
295
+ },
296
+ "btn_explain_command": {
297
+ "en": "Explain command",
298
+ "fr": "Expliquer la commande",
299
+ },
300
+ "quick_suggestions": {
301
+ "en": "QUICK ACTIONS",
302
+ "fr": "ACTIONS RAPIDES",
303
+ },
304
+ "action_ports_title": {
305
+ "en": "Ports & Processes",
306
+ "fr": "Ports & Processus",
307
+ },
308
+ "action_ports_desc": {
309
+ "en": "Find listening ports & active PIDs",
310
+ "fr": "Trouver les ports en écoute et PIDs",
311
+ },
312
+ "action_disk_title": {
313
+ "en": "Disk Usage",
314
+ "fr": "Espace Disque",
315
+ },
316
+ "action_disk_desc": {
317
+ "en": "Analyze current folder space usage",
318
+ "fr": "Analyser la taille du dossier courant",
319
+ },
320
+ "action_docker_title": {
321
+ "en": "Docker Cleanup",
322
+ "fr": "Nettoyage Docker",
323
+ },
324
+ "action_docker_desc": {
325
+ "en": "Prune unused containers & images",
326
+ "fr": "Purger conteneurs et images orphelines",
327
+ },
328
+ "action_git_title": {
329
+ "en": "Undo Git Commit",
330
+ "fr": "Annuler Commit Git",
331
+ },
332
+ "action_git_desc": {
333
+ "en": "Rollback commit without losing code",
334
+ "fr": "Rollback commit sans perdre de code",
335
+ },
336
+ "status_ready": {
337
+ "en": "Ready for commands and diagnostics",
338
+ "fr": "Prêt pour vos commandes et diagnostics",
339
+ },
340
+ "status_footer_hint": {
341
+ "en": "[↵ Enter] Ask / Run [Esc] Close [Alt+E] Shortcut",
342
+ "fr": "[↵ Entrée] Poser / Exécuter [Echap] Fermer [Alt+E] Raccourci",
343
+ },
344
+ }
345
+
346
+
347
+ class Translator:
348
+ """Provides translated strings for a specific language.
349
+
350
+ Usage::
351
+
352
+ t = Translator(Lang.FR)
353
+ print(t("section_cause")) # → "CAUSE"
354
+ print(t("welcome_title")) # → "ezShell v0.1.0"
355
+
356
+ The active language can also be initialised with auto-detection::
357
+
358
+ t = Translator.auto()
359
+
360
+ ---
361
+
362
+ Fournit les chaînes traduites pour une langue donnée.
363
+ Initialisation avec auto-détection : ``Translator.auto()``.
364
+ """
365
+
366
+ def __init__(self, lang: Lang = Lang.EN) -> None:
367
+ """Initialises the translator with the given language.
368
+
369
+ Args:
370
+ lang: The language to use for all lookups. Defaults to English.
371
+ """
372
+ self.lang = lang
373
+
374
+ @classmethod
375
+ def auto(cls) -> "Translator":
376
+ """Creates a Translator using the auto-detected system language.
377
+
378
+ Returns:
379
+ Translator: A new Translator instance for the detected language.
380
+ """
381
+ return cls(detect_language())
382
+
383
+ def __call__(self, key: str, fallback: Optional[str] = None) -> str:
384
+ """Looks up a translation by key.
385
+
386
+ Args:
387
+ key: The string key defined in the catalogue (e.g. ``"section_cause"``).
388
+ fallback: Optional string returned if the key is not in the catalogue.
389
+ Defaults to the key itself if omitted.
390
+
391
+ Returns:
392
+ str: The translated string for the active language.
393
+ """
394
+ entry = _STRINGS.get(key)
395
+ if entry is None:
396
+ return fallback if fallback is not None else key
397
+ return entry.get(self.lang.value, entry.get("en", key))
398
+
399
+ def get(self, key: str, fallback: Optional[str] = None) -> str:
400
+ """Alias for ``__call__`` — preferred for explicit usage.
401
+
402
+ Args:
403
+ key: Translation key.
404
+ fallback: Optional fallback string.
405
+
406
+ Returns:
407
+ str: Translated string.
408
+ """
409
+ return self(key, fallback)
410
+
411
+
412
+ # ---------------------------------------------------------------------------
413
+ # Module-level convenience singleton — used across ui modules
414
+ # ---------------------------------------------------------------------------
415
+
416
+ #: Global translator instance, auto-detected from system locale.
417
+ #: Import this directly: ``from src.i18n import t``
418
+ t: Translator = Translator.auto()
419
+
420
+
421
+ __all__ = ["Lang", "Translator", "detect_language", "t"]
422
+