Source code
Revision control
Copy as Markdown
Other Tools
#!/usr/bin/env python3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
"""
Generates a preferences navigation map by parsing CONFIG_PANES from
preferences.js and resolving display names and descriptions from .ftl files.
Run from the repo root:
python3 browser/components/aiwindow/models/scripts/generate_prefs_nav_map.py
Output:
browser/components/aiwindow/models/data/preferences-navigation.json
"""
import json
import re
from pathlib import Path
TOPSRCDIR = Path(__file__).resolve().parents[5]
PREFS_JS = TOPSRCDIR / "browser/components/preferences/preferences.js"
FTL_FILES = [
TOPSRCDIR / "browser/locales/en-US/browser/preferences/preferences.ftl",
TOPSRCDIR / "browser/locales-preview/aiFeatures.ftl",
]
OUTPUT = TOPSRCDIR / "browser/components/aiwindow/models/PreferencesNavMap.sys.mjs"
# Top-level panes not in CONFIG_PANES (registered via XUL).
TOPLEVEL_PANE_L10N_IDS = {
"general": "pane-general-title",
"home": "pane-home-title",
"search": "pane-search-title",
"privacy": "pane-privacy-title",
"sync": "pane-sync-title3",
}
FTL_VARIABLE_SUBSTITUTIONS = {
"-brand-short-name": "Firefox",
"-brand-product-name": "Firefox",
}
# Panes whose description lives on a sibling FTL entry rather than on the
# pane header l10nId itself. Maps paneId -> l10nId of the description source.
PANE_DESCRIPTION_L10N_IDS = {
"ai": "preferences-ai-controls-description",
"dnsOverHttps": "preferences-doh-description2",
"etp": "preferences-etp-status-header",
"history": "history-section-header",
"paneProfiles": "preferences-profiles-section-header",
"personalizeSmartWindow": "smart-window-model-section",
"translations": "settings-translations-header",
}
_BLOCK_RE = re.compile(r"^([\w-]+)\s*=[^\n]*\n((?:[ \t]+[^\n]*\n?)*)", re.MULTILINE)
def parse_config_panes(js_text):
"""Extract pane id -> {parent, l10nId} from the CONFIG_PANES block."""
block_match = re.search(
r"const CONFIG_PANES\s*=\s*Object\.freeze\(\{(.+?)\}\);",
js_text,
re.DOTALL,
)
if not block_match:
raise ValueError("CONFIG_PANES block not found in preferences.js")
panes = {}
for m in re.finditer(r"(\w+)\s*:\s*\{([^}]+)\}", block_match.group(1)):
pane_id, body = m.group(1), m.group(2)
parent_m = re.search(r'parent\s*:\s*["\'](\w+)["\']', body)
l10n_m = re.search(r'l10nId\s*:\s*["\']([^"\']+)["\']', body)
panes[pane_id] = {
"parent": parent_m.group(1) if parent_m else None,
"l10nId": l10n_m.group(1) if l10n_m else None,
}
return panes
def parse_ftl_strings(ftl_files):
"""Return (labels, descriptions) dicts resolved from .ftl files."""
raw_labels, raw_descriptions = {}, {}
for ftl_path in ftl_files:
text = ftl_path.read_text(encoding="utf-8")
# Capture single-line values including those with { FTL references }.
# Exclude lines starting with . (attributes) and [ (variants).
for m in re.finditer(r"^([\w-]+)\s*=\s*([^\n\[\.][^\n]*)$", text, re.MULTILINE):
if value := m.group(2).strip():
raw_labels.setdefault(m.group(1), value)
for m in _BLOCK_RE.finditer(text):
l10n_id, attrs = m.group(1), m.group(2)
if heading := re.search(r"\.heading\s*=\s*(.+)", attrs):
raw_labels[l10n_id] = heading.group(1).strip()
if desc := re.search(r"\.description\s*=\s*(.+)", attrs):
raw_descriptions[l10n_id] = desc.group(1).strip()
def resolve(value):
def replacer(ref_m):
ref = ref_m.group(1).strip()
return FTL_VARIABLE_SUBSTITUTIONS.get(ref) or raw_labels.get(ref, ref)
return re.sub(r"\{([^}]+)\}", replacer, value)
return (
{k: resolve(v) for k, v in raw_labels.items()},
{k: resolve(v) for k, v in raw_descriptions.items()},
)
def label_of(pane_id, panes, labels):
"""Resolve a display name for any pane id, including top-level ones."""
l10n_id = (panes.get(pane_id) or {}).get("l10nId") or TOPLEVEL_PANE_L10N_IDS.get(
pane_id, ""
)
return labels.get(l10n_id) or labels.get(f"pane-{pane_id}-title", pane_id)
def breadcrumb_for(pane_id, panes, labels):
"""Build 'Settings > Parent > Child' by walking up the parent chain."""
parts = []
current = pane_id
while current:
parts.append(label_of(current, panes, labels))
current = (panes.get(current) or {}).get("parent")
return " > ".join(["Settings", *reversed(parts)])
def description_for(pane_id, l10n_id, descriptions, labels):
"""
Return a description for a pane:
1. .description attribute directly on the pane's l10nId
2. Explicit PANE_DESCRIPTION_L10N_IDS override (description or label)
"""
if not l10n_id:
return None
if l10n_id in descriptions:
return descriptions[l10n_id]
override_id = PANE_DESCRIPTION_L10N_IDS.get(pane_id)
if override_id:
return descriptions.get(override_id) or labels.get(override_id)
return None
def main():
panes = parse_config_panes(PREFS_JS.read_text(encoding="utf-8"))
labels, descriptions = parse_ftl_strings(FTL_FILES)
nav_map = {
"about:preferences": {
"label": "Settings",
"breadcrumb": "Settings",
"description": None,
}
}
for pane_id, cfg in panes.items():
nav_map[f"about:preferences#{pane_id}"] = {
"label": label_of(pane_id, panes, labels),
"breadcrumb": breadcrumb_for(pane_id, panes, labels),
"description": description_for(
pane_id, cfg["l10nId"], descriptions, labels
),
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
json_str = json.dumps(nav_map, indent=2, ensure_ascii=False)
OUTPUT.write_text(
"// This Source Code Form is subject to the terms of the Mozilla Public\n"
"// License, v. 2.0. If a copy of the MPL was not distributed with this\n"
"// Auto-generated by browser/components/aiwindow/models/scripts/generate_prefs_nav_map.py\n"
"// Do not edit manually. Run the script to regenerate.\n\n"
f"export const PREFERENCES_NAV_MAP = {json_str};\n"
)
print(f"Generated {len(nav_map)} entries -> {OUTPUT.relative_to(TOPSRCDIR)}")
if __name__ == "__main__":
main()