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(