This commit is contained in:
Rio Griya Putra 2026-08-27 20:22:22 +00:00 committed by GitHub
commit 5040b81b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 272 additions and 0 deletions

View file

@ -36,6 +36,10 @@ for asset in viewer_static.rglob('*'):
rel_path = asset.relative_to(project_root)
datas.append((str(asset), str(rel_path.parent)))
for json_locale in strix_root.rglob('locales/*.json'):
rel_path = json_locale.relative_to(project_root)
datas.append((str(json_locale), str(rel_path.parent)))
datas += collect_data_files('tiktoken')
datas += collect_data_files('tiktoken_ext')
@ -112,6 +116,7 @@ hiddenimports = [
# Strix modules
'strix',
'strix.i18n',
'strix.interface',
'strix.interface.main',
'strix.interface.cli',

View file

@ -1,4 +1,9 @@
You are an advanced AI application security validation agent. Your purpose is to perform authorized security verification, reproduce and validate weaknesses on in-scope assets, and help remediate real security issues.
{% if language_directive %}
<language_directive>
{{ language_directive }}
</language_directive>
{% endif %}
You follow all instructions and rules provided to you exactly as written in the system prompt at all times.
{% if is_root %}
<root_agent_directive>

View file

@ -147,6 +147,10 @@ class ViewerSettings(BaseSettings):
class Settings(BaseSettings):
model_config = _BASE_CONFIG
language: str = Field(
default="en",
validation_alias=AliasChoices("STRIX_LANGUAGE", "language"),
)
llm: LlmSettings = Field(default_factory=LlmSettings)
dedupe: DedupeSettings = Field(default_factory=DedupeSettings)
runtime: RuntimeSettings = Field(default_factory=RuntimeSettings)

145
strix/i18n.py Normal file
View file

@ -0,0 +1,145 @@
"""Robust Internationalization (i18n) Engine for Strix."""
from __future__ import annotations
import contextvars
import json
import os
from pathlib import Path
import threading
from typing import Any, Dict, Optional, Set
SUPPORTED_LANGUAGES: Set[str] = {"en", "es", "id"}
DEFAULT_LANGUAGE: str = "en"
# ContextVar for async execution & multi-tenant thread safety
_current_language: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
"strix_current_language", default=None
)
_locales_lock = threading.Lock()
_locales_cache: Dict[str, Dict[str, str]] = {}
def _normalize_lang(lang: Optional[str]) -> str:
"""Normalize language tags (e.g. 'es_ES.UTF-8' -> 'es')."""
if not lang:
return DEFAULT_LANGUAGE
clean = lang.strip().lower().split("_")[0].split("-")[0]
return clean if clean in SUPPORTED_LANGUAGES else DEFAULT_LANGUAGE
def set_language(lang: Optional[str]) -> str:
"""Set language preference for the current async execution context."""
normalized = _normalize_lang(lang)
_current_language.set(normalized)
return normalized
def get_language() -> str:
"""Get active language preference or auto-detect from environment/config."""
lang = _current_language.get()
if lang is not None:
return lang
return _detect_language()
def _detect_language() -> str:
"""Detect language via STRIX_LANGUAGE, config file, or system LANG/LC_ALL."""
# 1. Environment Variable STRIX_LANGUAGE
env_lang = os.getenv("STRIX_LANGUAGE")
if env_lang:
return _normalize_lang(env_lang)
# 2. Config File ~/.strix/cli-config.json
try:
cfg_path = Path.home() / ".strix" / "cli-config.json"
if cfg_path.exists():
data = json.loads(cfg_path.read_text(encoding="utf-8"))
cfg_lang = data.get("env", {}).get("STRIX_LANGUAGE") or data.get("language")
if cfg_lang:
return _normalize_lang(cfg_lang)
except Exception:
pass
# 3. System Locale (LC_ALL takes precedence over LANG)
sys_lang = os.getenv("LC_ALL") or os.getenv("LANG")
if sys_lang:
return _normalize_lang(sys_lang)
return DEFAULT_LANGUAGE
def _load_locale(lang: str) -> Dict[str, str]:
"""Load locale JSON file with thread-safe caching, type validation, and user overrides."""
with _locales_lock:
if lang in _locales_cache:
return _locales_cache[lang]
translations: Dict[str, str] = {}
def _safe_merge(source_data: Any) -> None:
if isinstance(source_data, dict):
for k, v in source_data.items():
if isinstance(k, str) and isinstance(v, str):
translations[k] = v
# Built-in locale
builtin_path = Path(__file__).parent / "locales" / f"{lang}.json"
if builtin_path.exists():
try:
_safe_merge(json.loads(builtin_path.read_text(encoding="utf-8")))
except Exception:
pass
# User override locale (~/.strix/locales/{lang}.json)
user_path = Path.home() / ".strix" / "locales" / f"{lang}.json"
if user_path.exists():
try:
_safe_merge(json.loads(user_path.read_text(encoding="utf-8")))
except Exception:
pass
with _locales_lock:
_locales_cache[lang] = translations
return translations
def t(key: str, **kwargs: Any) -> str:
"""Translate key into active language with safe keyword interpolation."""
lang = get_language()
locale = _load_locale(lang)
msg = locale.get(key)
if msg is None and lang != DEFAULT_LANGUAGE:
msg = _load_locale(DEFAULT_LANGUAGE).get(key)
if msg is None:
msg = key
if kwargs and "{" in msg and "}" in msg:
try:
msg = msg.format(**kwargs)
except (KeyError, ValueError, IndexError):
for k, v in kwargs.items():
msg = msg.replace(f"{{{k}}}", str(v))
return msg
def get_language_directive() -> str:
"""Generate prompt directive for LLM agents when target language is non-English."""
lang = get_language()
if lang == "en":
return ""
lang_names = {
"es": "Spanish",
"id": "Indonesian",
}
target_name = lang_names.get(lang, lang.upper())
return (
f"IMPORTANT LANGUAGE INSTRUCTION:\n"
f"Write all human-readable findings, executive summaries, descriptions, and remediation steps in {target_name}.\n"
f"Do NOT translate technical identifiers, including CVE IDs, CWE IDs, CVSS scores, "
f"source code snippets, file paths, or shell commands."
)

View file

@ -52,6 +52,13 @@ def _positive_int(value: str) -> int:
def parse_arguments() -> argparse.Namespace:
# Pre-resolve language before argparse if set in sys.argv
for idx, arg in enumerate(sys.argv[:-1]):
if arg in ("--language", "-l") and idx + 1 < len(sys.argv):
from strix.i18n import set_language
set_language(sys.argv[idx + 1])
break
parser = argparse.ArgumentParser(
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
@ -114,6 +121,12 @@ Examples:
help="Update strix to the latest version and exit. Self-updates the "
"standalone binary install; for pip/pipx/uv installs, prints the "
"matching upgrade command instead.",
parser.add_argument(
"--language",
"-l",
type=str,
default=None,
help="Language preference for CLI messages and audit reports (e.g. en, es, id)",
)
parser.add_argument(

11
strix/locales/en.json Normal file
View file

@ -0,0 +1,11 @@
{
"cli.description": "Strix - Autonomous AI Security Penetration Testing Agent",
"cli.scan_started": "Starting scan on target: {target}",
"cli.scan_completed": "Scan completed for target: {target}",
"cli.vulnerabilities_found": "Found {count} vulnerability(ies)",
"report.title": "Security Audit Executive Report",
"report.description": "Description",
"report.evidence": "Evidence",
"report.impact": "Impact",
"report.remediation": "Remediation Steps"
}

11
strix/locales/es.json Normal file
View file

@ -0,0 +1,11 @@
{
"cli.description": "Strix - Agente Autónomo de Pruebas de Penetración de Seguridad con IA",
"cli.scan_started": "Iniciando escaneo en el objetivo: {target}",
"cli.scan_completed": "Escaneo completado para el objetivo: {target}",
"cli.vulnerabilities_found": "Se encontraron {count} vulnerabilidad(es)",
"report.title": "Informe Ejecutivo de Auditoría de Seguridad",
"report.description": "Descripción",
"report.evidence": "Evidencia",
"report.impact": "Impacto",
"report.remediation": "Pasos de Remediación"
}

11
strix/locales/id.json Normal file
View file

@ -0,0 +1,11 @@
{
"cli.description": "Strix - Agen Uji Penetrasi Keamanan Berbasis AI Otonom",
"cli.scan_started": "Memulai pemindaian pada target: {target}",
"cli.scan_completed": "Pemindaian selesai untuk target: {target}",
"cli.vulnerabilities_found": "Ditemukan {count} kerentanan",
"report.title": "Laporan Eksekutif Audit Keamanan",
"report.description": "Deskripsi",
"report.evidence": "Bukti",
"report.impact": "Dampak",
"report.remediation": "Langkah Perbaikan"
}

67
tests/test_i18n.py Normal file
View file

@ -0,0 +1,67 @@
"""Comprehensive tests for the Strix i18n engine."""
import asyncio
import os
from unittest.mock import patch
import pytest
import strix.i18n as i18n
from strix.i18n import get_language, get_language_directive, set_language, t
@pytest.fixture(autouse=True)
def _reset_i18n():
i18n._current_language.set(None)
with i18n._locales_lock:
i18n._locales_cache.clear()
yield
i18n._current_language.set(None)
def test_set_and_get_language():
set_language("es")
assert get_language() == "es"
set_language("id")
assert get_language() == "id"
def test_fallback_unsupported_language():
set_language("fr")
assert get_language() == "en"
def test_translation_formatting():
set_language("id")
result = t("cli.scan_started", target="example.com")
assert "example.com" in result
assert "Memulai" in result
def test_safe_interpolation_on_missing_key():
set_language("en")
result = t("cli.scan_started") # target missing
assert "target" in result or isinstance(result, str)
@pytest.mark.asyncio
async def test_async_contextvar_isolation():
"""Verify concurrent async tasks maintain isolated language contexts."""
async def task_es():
set_language("es")
await asyncio.sleep(0.01)
assert get_language() == "es"
async def task_id():
set_language("id")
await asyncio.sleep(0.01)
assert get_language() == "id"
await asyncio.gather(task_es(), task_id())
def test_language_directive():
set_language("es")
directive = get_language_directive()
assert "Spanish" in directive
assert "CVE" in directive