Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PythonScripts/audit_translations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The tool analyzes rule files to detect the following issues:
* **Extra Rules:** Rules present in the target translation but absent in the source (flagged as potentially intentional language-specific additions).
* **Untranslated Text:** Detects text keys that still use **lowercase** formatting, indicating they haven't been verified or translated yet.
* **Rule Differences:** Structural changes (match expressions, conditions, variables, or test/replace layout) between the source and target translation.
* **Rule Order:** After the summary, reports the first differing position of shared named rules in each affected file. Missing, extra, and `# audit-ignore` rules do not shift the comparison. Include entries are not checked.
* **Definition Coverage:** Compares literal `definitions.yaml` entries by name and collection kind (`vector`, `set`, or `map`).

After a blank line, add `# audit-ignore` immediately before a rule to suppress
Expand Down
20 changes: 20 additions & 0 deletions PythonScripts/audit_translations/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
print_audit_summary,
print_definition_findings,
print_language_list,
print_order_findings,
print_warnings,
)

Expand Down Expand Up @@ -109,6 +110,19 @@ def merge_rules(base_rules: list[RuleInfo], region_rules: list[RuleInfo]) -> lis
include_untranslated = include_all or "untranslated" in issue_filter
include_extra = include_all or "extra" in issue_filter
include_diffs = include_all or "diffs" in issue_filter
include_order = include_all

first_order_mismatch_position = None
if include_order:
source_keys = {rule.key for rule in english_rules if rule.name is not None and not rule.audit_ignore}
target_keys = {rule.key for rule in translated_rules if rule.name is not None and not rule.audit_ignore}
shared_keys = source_keys & target_keys
source_order = [rule for rule in english_rules if rule.key in shared_keys and not rule.audit_ignore]
target_order = [rule for rule in translated_rules if rule.key in shared_keys and not rule.audit_ignore]
for position, (source_rule, target_rule) in enumerate(zip(source_order, target_order), start=1):
if source_rule.key != target_rule.key:
first_order_mismatch_position = position
break

# Find missing rules (in source but not in translation)
missing_rules = []
Expand Down Expand Up @@ -146,6 +160,7 @@ def merge_rules(base_rules: list[RuleInfo], region_rules: list[RuleInfo]) -> lis
extra_rules=extra_rules,
untranslated_text=untranslated_text,
rule_differences=rule_differences,
first_order_mismatch_position=first_order_mismatch_position,
english_rule_count=len(english_rules),
translated_rule_count=len(translated_rules),
)
Expand Down Expand Up @@ -262,6 +277,7 @@ def audit_language(
total_definition_type_mismatches = 0
files_with_issues = 0
files_ok = 0
order_findings: list[tuple[str, int]] = [] # One item per file, eg [("calculus.yaml", 2), ...]

for file_name in files:
english_path = source_dir / file_name
Expand Down Expand Up @@ -308,6 +324,8 @@ def audit_language(
existing_translated_region_path,
existing_english_region_path,
)
if result.first_order_mismatch_position is not None:
order_findings.append((file_name, result.first_order_mismatch_position))

if result.has_issues:
issues = print_warnings(result, file_name, verbose, language, source_language)
Expand Down Expand Up @@ -338,6 +356,8 @@ def audit_language(
)
)

print_order_findings(order_findings)

return total_issues


Expand Down
1 change: 1 addition & 0 deletions PythonScripts/audit_translations/models/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class ComparisonResult:
english_rule_count: int
translated_rule_count: int
rule_differences: list[RuleDifference] = field(default_factory=list)
first_order_mismatch_position: int | None = None

@property
def has_issues(self) -> bool:
Expand Down
9 changes: 9 additions & 0 deletions PythonScripts/audit_translations/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ def print_audit_summary(summary: AuditSummary) -> None:
console.print(Panel(table, style="cyan"))


def print_order_findings(findings: list[tuple[str, int]]) -> None:
"""Print the first shared-rule order difference for each affected file."""
for file_name, position in findings:
console.print(
f"[yellow]Rule order differs[/] in {escape(str(file_name))}: "
f"first difference at shared rule position {position}"
)


def print_language_list(languages: list[tuple[str, int]]) -> None:
"""Print the available languages table.

Expand Down
127 changes: 127 additions & 0 deletions PythonScripts/audit_translations/tests/test_auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,133 @@ def fixture_rules_dir() -> Path:
return Path(__file__).resolve().parent / "fixtures" / "Rules" / "Languages"


def test_rule_order_reports_first_shared_difference(tmp_path) -> None:
"""A swapped pair records the first shared rule position without adding an issue."""
source = tmp_path / "source.yaml"
target = tmp_path / "target.yaml"
source.write_text("""- name: a
tag: mi
- name: b
tag: mi
- name: c
tag: mi
""", encoding="utf-8")
target.write_text("""- name: a
tag: mi
- name: c
tag: mi
- name: b
tag: mi
""", encoding="utf-8")

result = compare_files(source, target)
assert result.first_order_mismatch_position == 2
assert not result.has_issues


def test_missing_and_ignored_rules_do_not_shift_order(tmp_path) -> None:
"""Only shared rules without audit-ignore markers participate in ordering."""
source = tmp_path / "source.yaml"
target = tmp_path / "target.yaml"
source.write_text("""- name: a
tag: mi
- name: missing
tag: mi
- name: b
tag: mi
""", encoding="utf-8")
target.write_text("""- name: a
tag: mi
- name: extra
tag: mi
- name: b
tag: mi
""", encoding="utf-8")
assert compare_files(source, target).first_order_mismatch_position is None

source.write_text("""- name: a
tag: mi
- name: b
tag: mi
# audit-ignore
- name: c
tag: mi
""", encoding="utf-8")
target.write_text("""# audit-ignore
- name: c
tag: mi
- name: a
tag: mi
- name: b
tag: mi
""", encoding="utf-8")
assert compare_files(source, target).first_order_mismatch_position is None


def test_include_placement_is_not_checked(tmp_path) -> None:
"""An include moving around a named rule has no effect on named rule order."""
source = tmp_path / "source.yaml"
target = tmp_path / "target.yaml"
source.write_text("""- include: shared.yaml
- name: a
tag: mi
""", encoding="utf-8")
target.write_text("""- name: a
tag: mi
- include: shared.yaml
""", encoding="utf-8")
result = compare_files(source, target)
assert result.first_order_mismatch_position is None
assert result.english_rule_count == result.translated_rule_count == 1
assert not result.missing_rules and not result.extra_rules
assert compare_files(source, target, {"missing"}).first_order_mismatch_position is None


def test_unicode_entries_are_not_checked_for_order(tmp_path) -> None:
"""The order check only covers named rules, not Unicode character entries."""
source = tmp_path / "unicode.yaml"
target = tmp_path / "target" / "unicode.yaml"
target.parent.mkdir()
source.write_text("""- "←": [t: left]
- "∥": [t: parallel]
""", encoding="utf-8")
target.write_text("""- "∥": [T: parallel]
- "←": [T: left]
""", encoding="utf-8")

assert compare_files(source, target).first_order_mismatch_position is None


def test_order_findings_appear_after_summary_without_affecting_totals(tmp_path, capsys) -> None:
"""Each affected file gets one final line while the audit issue count stays zero."""
rules_dir = tmp_path / "Rules" / "Languages"
source_dir = rules_dir / "en"
target_dir = rules_dir / "de"
source_dir.mkdir(parents=True)
target_dir.mkdir(parents=True)
source = """- name: a
tag: mi
- name: b
tag: mi
"""
target = """- name: b
tag: mi
- name: a
tag: mi
"""
for filename in ("first.yaml", "second.yaml"):
(source_dir / filename).write_text(source, encoding="utf-8")
(target_dir / filename).write_text(target, encoding="utf-8")

assert audit_language("de", rules_dir=str(rules_dir)) == 0
output = strip_ansi(capsys.readouterr().out)
assert "Files with issues 0" in output
assert "Rule order differences" not in output
assert output.count("first difference at shared rule position 1") == 2
assert output.index("SUMMARY") < output.index("Rule order differs in first.yaml")
assert output.index("Rule order differs in first.yaml") < output.index("Rule order differs in second.yaml")


def aggregate_issue_counts(
language: str,
issue_filter: set[str] | None = None,
Expand Down
2 changes: 1 addition & 1 deletion PythonScripts/audit_translations/tests/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def test_skips_items_without_name(self):
assert rules[0].name == "real-rule"

def test_mixed_valid_and_skipped_items(self):
"""Valid rules interspersed with non-rule items keep correct line numbers."""
"""Named rules retain their line numbers around skipped includes."""
content = """- name: first
tag: mo
match: "."
Expand Down
Loading