|
19 | 19 | import json |
20 | 20 | import locale |
21 | 21 | import os |
| 22 | +import re |
22 | 23 | from typing import Any |
23 | 24 |
|
24 | | -from engine_sdk.utils import log_i18n_level |
25 | | - |
26 | 25 | # Built-in fallback for English if language files are missing |
27 | 26 | FALLBACK_EN: dict[str, Any] = { |
28 | 27 | "name": "English", |
@@ -534,6 +533,258 @@ def tr_fr_en(gui: object | None, fr: str, en: str) -> str: |
534 | 533 | return en |
535 | 534 |
|
536 | 535 |
|
| 536 | +# ------------------------------- |
| 537 | +# i18n-aware logging helpers |
| 538 | +# (moved from engine_sdk.utils) |
| 539 | +# ------------------------------- |
| 540 | + |
| 541 | +_REDACT_PATTERNS = [ |
| 542 | + re.compile(r"(password\s*[:=]\s*)([^\s]+)", re.IGNORECASE), |
| 543 | + re.compile(r"(authorization\s*[:]\s*bearer\s+)([A-Za-z0-9\-_.]+)", re.IGNORECASE), |
| 544 | + re.compile(r"(token\s*[:=]\s*)([A-Za-z0-9\-_.]{12,})", re.IGNORECASE), |
| 545 | +] |
| 546 | + |
| 547 | + |
| 548 | +def redact_secrets(text: str) -> str: |
| 549 | + """Return text with obvious secrets masked to avoid log leakage.""" |
| 550 | + if not text: |
| 551 | + return text |
| 552 | + redacted = str(text) |
| 553 | + try: |
| 554 | + for pat in _REDACT_PATTERNS: |
| 555 | + redacted = pat.sub(lambda m: m.group(1) + "<redacted>", redacted) |
| 556 | + except Exception: |
| 557 | + pass |
| 558 | + return redacted |
| 559 | + |
| 560 | + |
| 561 | +def clamp_text(text: str, *, max_len: int = 10000) -> str: |
| 562 | + """Clamp long text to max_len characters (suffix with …).""" |
| 563 | + if text is None: |
| 564 | + return "" |
| 565 | + s = str(text) |
| 566 | + return s if len(s) <= max_len else (s[: max_len - 1] + "…") |
| 567 | + |
| 568 | + |
| 569 | +def tr(gui: Any, fr: str, en: str) -> str: |
| 570 | + """Robust translator wrapper using the host GUI translator when available.""" |
| 571 | + try: |
| 572 | + fn = getattr(gui, "tr", None) |
| 573 | + if callable(fn): |
| 574 | + return fn(fr, en) |
| 575 | + except Exception: |
| 576 | + pass |
| 577 | + return tr_fr_en(gui, fr, en) |
| 578 | + |
| 579 | + |
| 580 | +essential_log_max_len = 10000 |
| 581 | + |
| 582 | + |
| 583 | +def safe_log(gui: Any, text: str, *, redact: bool = True, clamp: bool = True) -> None: |
| 584 | + """Append text to GUI log safely (or print), with optional redaction and clamping.""" |
| 585 | + try: |
| 586 | + msg = str(text) |
| 587 | + if redact: |
| 588 | + msg = redact_secrets(msg) |
| 589 | + if clamp: |
| 590 | + msg = clamp_text(msg, max_len=essential_log_max_len) |
| 591 | + if hasattr(gui, "log") and getattr(gui, "log") is not None: |
| 592 | + try: |
| 593 | + gui.log.append(msg) |
| 594 | + return |
| 595 | + except Exception: |
| 596 | + pass |
| 597 | + print(msg) |
| 598 | + except Exception: |
| 599 | + try: |
| 600 | + print(text) |
| 601 | + except Exception: |
| 602 | + pass |
| 603 | + |
| 604 | + |
| 605 | +_LOG_LEVEL_LABELS = { |
| 606 | + "info": "INFO", |
| 607 | + "warning": "WARN", |
| 608 | + "error": "ERROR", |
| 609 | + "success": "SUCCESS", |
| 610 | + "state": "STATE", |
| 611 | +} |
| 612 | + |
| 613 | +_LOG_LEVEL_RICH = { |
| 614 | + "info": "cyan", |
| 615 | + "warning": "yellow", |
| 616 | + "error": "red", |
| 617 | + "success": "green", |
| 618 | + "state": "blue", |
| 619 | +} |
| 620 | + |
| 621 | +_LOG_LEVEL_COLORAMA = { |
| 622 | + "info": "CYAN", |
| 623 | + "warning": "YELLOW", |
| 624 | + "error": "RED", |
| 625 | + "success": "GREEN", |
| 626 | + "state": "BLUE", |
| 627 | +} |
| 628 | + |
| 629 | +_LOG_LEVEL_QT_COLORS = { |
| 630 | + "info": "#1E88E5", |
| 631 | + "warning": "#EF6C00", |
| 632 | + "error": "#D32F2F", |
| 633 | + "success": "#2E7D32", |
| 634 | + "state": "#00897B", |
| 635 | + "debug": "#546E7A", |
| 636 | +} |
| 637 | + |
| 638 | +_RICH_CONSOLE = None |
| 639 | +_COLORAMA_READY = False |
| 640 | + |
| 641 | + |
| 642 | +def _get_rich_console(): |
| 643 | + global _RICH_CONSOLE |
| 644 | + if _RICH_CONSOLE is None: |
| 645 | + from rich.console import Console # type: ignore |
| 646 | + |
| 647 | + _RICH_CONSOLE = Console() |
| 648 | + return _RICH_CONSOLE |
| 649 | + |
| 650 | + |
| 651 | +def _console_log(level: str, label: str, message: str) -> None: |
| 652 | + # Prefer rich when available for consistent styling. |
| 653 | + try: |
| 654 | + console = _get_rich_console() |
| 655 | + style = _LOG_LEVEL_RICH.get(level, "white") |
| 656 | + console.print(f"[{style}]{label}[/] {message}") |
| 657 | + return |
| 658 | + except Exception: |
| 659 | + pass |
| 660 | + |
| 661 | + # Fallback to colorama for basic ANSI colors. |
| 662 | + try: |
| 663 | + from colorama import Fore, Style, init # type: ignore |
| 664 | + |
| 665 | + global _COLORAMA_READY |
| 666 | + if not _COLORAMA_READY: |
| 667 | + init(autoreset=True) |
| 668 | + _COLORAMA_READY = True |
| 669 | + |
| 670 | + color_name = _LOG_LEVEL_COLORAMA.get(level) |
| 671 | + color = getattr(Fore, color_name, "") |
| 672 | + if color: |
| 673 | + print(f"{color}{label}{Style.RESET_ALL} {message}") |
| 674 | + else: |
| 675 | + print(f"{label} {message}") |
| 676 | + return |
| 677 | + except Exception: |
| 678 | + pass |
| 679 | + |
| 680 | + print(f"{label} {message}") |
| 681 | + |
| 682 | + |
| 683 | +def _append_gui_log(gui: Any, level: str, label: str, msg: str) -> bool: |
| 684 | + """Append a log line to the GUI log, with color when possible.""" |
| 685 | + try: |
| 686 | + log = getattr(gui, "log", None) |
| 687 | + except Exception: |
| 688 | + return False |
| 689 | + if log is None: |
| 690 | + return False |
| 691 | + |
| 692 | + line = f"[{label}] {msg}" |
| 693 | + |
| 694 | + # List-backed logs (tests) |
| 695 | + try: |
| 696 | + if isinstance(log, list): |
| 697 | + log.append(line) |
| 698 | + return True |
| 699 | + except Exception: |
| 700 | + pass |
| 701 | + |
| 702 | + # QTextEdit with rich formatting |
| 703 | + try: |
| 704 | + if hasattr(log, "textCursor") and callable(log.textCursor): |
| 705 | + try: |
| 706 | + from PySide6.QtGui import QColor, QTextCharFormat, QTextCursor |
| 707 | + except Exception: |
| 708 | + return False |
| 709 | + cursor = log.textCursor() |
| 710 | + cursor.movePosition(QTextCursor.End) |
| 711 | + fmt = QTextCharFormat() |
| 712 | + color = _LOG_LEVEL_QT_COLORS.get(level) |
| 713 | + if color: |
| 714 | + fmt.setForeground(QColor(color)) |
| 715 | + cursor.insertText(line, fmt) |
| 716 | + cursor.insertText("\n") |
| 717 | + try: |
| 718 | + log.setTextCursor(cursor) |
| 719 | + log.ensureCursorVisible() |
| 720 | + except Exception: |
| 721 | + pass |
| 722 | + return True |
| 723 | + except Exception: |
| 724 | + pass |
| 725 | + |
| 726 | + # QPlainTextEdit / generic append |
| 727 | + try: |
| 728 | + if hasattr(log, "appendPlainText") and callable(log.appendPlainText): |
| 729 | + log.appendPlainText(line) |
| 730 | + return True |
| 731 | + except Exception: |
| 732 | + pass |
| 733 | + |
| 734 | + try: |
| 735 | + if hasattr(log, "append") and callable(log.append): |
| 736 | + log.append(line) |
| 737 | + return True |
| 738 | + except Exception: |
| 739 | + pass |
| 740 | + |
| 741 | + return False |
| 742 | + |
| 743 | + |
| 744 | +def log_with_level( |
| 745 | + gui: Any, |
| 746 | + level: str, |
| 747 | + message: str, |
| 748 | + *, |
| 749 | + redact: bool = True, |
| 750 | + clamp: bool = True, |
| 751 | +) -> None: |
| 752 | + """Append a level-tagged message to GUI log or print with colors in console.""" |
| 753 | + try: |
| 754 | + lvl = str(level).lower() if level is not None else "info" |
| 755 | + except Exception: |
| 756 | + lvl = "info" |
| 757 | + label = _LOG_LEVEL_LABELS.get(lvl, str(level).upper()) |
| 758 | + |
| 759 | + msg = str(message) if message is not None else "" |
| 760 | + if redact: |
| 761 | + msg = redact_secrets(msg) |
| 762 | + if clamp: |
| 763 | + msg = clamp_text(msg, max_len=essential_log_max_len) |
| 764 | + |
| 765 | + try: |
| 766 | + if _append_gui_log(gui, lvl, label, msg): |
| 767 | + return |
| 768 | + except Exception: |
| 769 | + pass |
| 770 | + |
| 771 | + _console_log(lvl, label, msg) |
| 772 | + |
| 773 | + |
| 774 | +def log_i18n_level( |
| 775 | + gui: Any, |
| 776 | + level: str, |
| 777 | + fr: str, |
| 778 | + en: str, |
| 779 | + *, |
| 780 | + redact: bool = True, |
| 781 | + clamp: bool = True, |
| 782 | +) -> None: |
| 783 | + """Translate then log a level-tagged message.""" |
| 784 | + msg = tr(gui, fr, en) |
| 785 | + log_with_level(gui, level, msg, redact=redact, clamp=clamp) |
| 786 | + |
| 787 | + |
537 | 788 | def apply_language(self, lang_display: str) -> None: |
538 | 789 | """Applique la langue sélectionnée (centralisé).""" |
539 | 790 | from .Globals import _run_coro_async |
|
0 commit comments