diff --git a/strix.spec b/strix.spec index 827e5e2c..fad16499 100644 --- a/strix.spec +++ b/strix.spec @@ -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', diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 23493d2d..f545a08e 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -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 }} + +{% endif %} You follow all instructions and rules provided to you exactly as written in the system prompt at all times. {% if is_root %} diff --git a/strix/config/settings.py b/strix/config/settings.py index 42a2c97e..0e20af47 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -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) diff --git a/strix/i18n.py b/strix/i18n.py index 07a0ab6b..0a280291 100644 --- a/strix/i18n.py +++ b/strix/i18n.py @@ -45,11 +45,13 @@ def get_language() -> str: def _detect_language() -> str: - """Detect language via STRIX_LANGUAGE, config file, or system LANG.""" + """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(): @@ -60,7 +62,8 @@ def _detect_language() -> str: except Exception: pass - sys_lang = os.getenv("LANG") or os.getenv("LC_ALL") + # 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) @@ -68,24 +71,32 @@ def _detect_language() -> str: def _load_locale(lang: str) -> Dict[str, str]: - """Load locale JSON file with thread-safe caching and user overrides.""" + """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: - translations.update(json.loads(builtin_path.read_text(encoding="utf-8"))) + _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: - translations.update(json.loads(user_path.read_text(encoding="utf-8"))) + _safe_merge(json.loads(user_path.read_text(encoding="utf-8"))) except Exception: pass diff --git a/strix/interface/cli_args.py b/strix/interface/cli_args.py index 6c672437..af0563d4 100644 --- a/strix/interface/cli_args.py +++ b/strix/interface/cli_args.py @@ -50,6 +50,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, @@ -108,6 +115,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(