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
11 changes: 10 additions & 1 deletion PythonScripts/audit_translations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ uv run --project PythonScripts audit-translations --list
* Region variants are shown as `lang-region` (e.g., `zz-aa`) based on subdirectories under `Rules/Languages/<lang>`.
* `--source`: Sets the source/reference language. Defaults to `en`.
* `--file`: Audits a single specific file instead of the whole directory.
* `--exclude`: Exclude one or more files from the audit.
* `--rules-dir`: Override the Rules/Languages directory path.
* `--only`: Filter issue types (comma-separated): `missing`, `untranslated`, `extra`, `diffs`, `all`.
* `--verbose`: Show detailed output including source/target snippets for rule differences.
Expand All @@ -100,9 +101,17 @@ uv run audit-translations de
# Compare Norwegian Bokmal against Swedish instead of English
uv run audit-translations nb --source sv

# Audit only a specific file
# Audit only a specific file (note: --file is incompatible with --exclude)
uv run audit-translations es --file ClearSpeak_Rules.yaml
uv run audit-translations es --file SharedRules/default.yaml

# Exclude a list of files from the audit, note that if you use this option before specifying the
# target language you'll need to use the option terminator (--).
# (note: --exclude is incompatible with --file)
uv run audit-translations es --exclude unicode-full.yaml
uv run audit-translations es --exclude unicode-full.yaml unicode.yaml
uv run audit-translations --exclude unicode-full.yaml -- es

# Audit a regional variant (merges Rules/Languages/de and Rules/Languages/de/CH)
uv run audit-translations de-CH

Expand Down
34 changes: 21 additions & 13 deletions PythonScripts/audit_translations/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,31 @@ def is_definitions_file(file_path: str | Path) -> bool:
return Path(file_path).name == "definitions.yaml"


def get_yaml_files(lang_dir: Path, region_dir: Path | None = None) -> list[Path]:
def get_yaml_files(lang_dir: Path, region_dir: Path | None = None, excluded_files: list[str] | None = None) -> list[Path]:
"""Get all YAML files to audit for a language, including region overrides."""
files: set[Path] = set()

def collect_from(directory: Path, root: Path) -> None:
def collect_from(directory: Path, root: Path, excluded: list[str] | None) -> None:
if not directory.exists():
return
for f in directory.glob("*.yaml"):
if f.name != "prefs.yaml":
files.add(f.relative_to(root))
shared_dir = directory / "SharedRules"
if shared_dir.exists():
for f in shared_dir.glob("*.yaml"):
files.add(f.relative_to(root))

collect_from(lang_dir, lang_dir)

excluded_paths = {Path(path) for path in excluded or []}

candidates = {f for f in directory.glob("*.yaml") if f.name != "prefs.yaml"}
candidates.update((directory / "SharedRules").glob("*.yaml"))

relative_candidates = {f.relative_to(directory): f for f in candidates}

for path in excluded_paths - relative_candidates.keys():
console.print(f"\n[yellow]⚠ Warning:[/] File to exclude {path} does not exist.")

files.update(
f.relative_to(root) for relative_path, f in relative_candidates.items() if relative_path not in excluded_paths
)

collect_from(lang_dir, lang_dir, excluded_files)
if region_dir:
collect_from(region_dir, region_dir)
collect_from(region_dir, region_dir, excluded_files)

return sorted(files)

Expand Down Expand Up @@ -215,6 +222,7 @@ def merge_definitions(
def audit_language(
language: str,
specific_file: str | None = None,
excluded_files: list[str] | None = None,
rules_dir: str | None = None,
issue_filter: set[str] | None = None,
verbose: bool = False,
Expand Down Expand Up @@ -248,7 +256,7 @@ def audit_language(
raise AuditError(f"Target region directory not found: {translated_region_dir}")

# Get list of files to audit
files = [specific_file] if specific_file else get_yaml_files(source_dir, source_region_dir)
files = [specific_file] if specific_file else get_yaml_files(source_dir, source_region_dir, excluded_files)

print_audit_header(language, len(files), source_language)

Expand Down
14 changes: 13 additions & 1 deletion PythonScripts/audit_translations/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ def main() -> None:

parser.add_argument("language", nargs="?", help="Language code to audit (e.g., 'es', 'de', 'fi')")
parser.add_argument("--source", default="en", help="Source/reference language code (default: 'en')")
parser.add_argument("--file", dest="specific_file", help="Audit only a specific file (e.g., 'SharedRules/default.yaml')")
file_group = parser.add_mutually_exclusive_group()
file_group.add_argument(
"--file",
dest="specific_file",
help="Audit only a specific file (e.g., 'SharedRules/default.yaml')",
)
file_group.add_argument(
"--exclude",
nargs="+",
dest="excluded_files",
help="Exclude a list of files from the audit.",
)
parser.add_argument("--list", action="store_true", help="List available languages")
parser.add_argument("--rules-dir", help="Override Rules/Languages directory path")
parser.add_argument(
Expand Down Expand Up @@ -67,6 +78,7 @@ def main() -> None:
audit_language(
args.language,
args.specific_file,
args.excluded_files,
args.rules_dir,
issue_filter,
args.verbose,
Expand Down
74 changes: 74 additions & 0 deletions PythonScripts/audit_translations/tests/test_cli_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,80 @@ def test_cli_main_rich_only_filters_issue_groups(capsys, monkeypatch) -> None:
assert "Structure Differences" not in output


@pytest.mark.parametrize(
("excluded_file", "expected_file", "expected_rule", "unexpected_rule"),
[
(
"default.yaml",
"SharedRules/default.yaml",
"shared-only",
"root-only",
),
(
"SharedRules/default.yaml",
"default.yaml",
"root-only",
"shared-only",
),
],
ids=["exclude-root", "exclude-shared-rules"],
)
def test_cli_main_exclude_uses_relative_paths(
tmp_path,
capsys,
monkeypatch,
excluded_file,
expected_file,
expected_rule,
unexpected_rule,
) -> None:
"""Ensure --exclude distinguishes root files from SharedRules files."""
rules_dir = tmp_path / "Rules" / "Languages"
source_dir = rules_dir / "en"
target_dir = rules_dir / "de"

(source_dir / "SharedRules").mkdir(parents=True)
(target_dir / "SharedRules").mkdir(parents=True)

(source_dir / "default.yaml").write_text(
'- name: root-only\n tag: mo\n match: ".//m:mi"\n replace:\n - t: "root"\n',
encoding="utf-8",
)
(source_dir / "SharedRules" / "default.yaml").write_text(
'- name: shared-only\n tag: mo\n match: ".//m:mi"\n replace:\n - t: "shared"\n',
encoding="utf-8",
)

target_rule = '- name: target-only\n tag: mo\n match: ".//m:mi"\n replace:\n - t: "target"\n'
(target_dir / "default.yaml").write_text(
target_rule,
encoding="utf-8",
)
(target_dir / "SharedRules" / "default.yaml").write_text(
target_rule,
encoding="utf-8",
)

args = [
"de",
"--rules-dir",
str(rules_dir),
"--exclude",
excluded_file,
"--only",
"missing",
]
monkeypatch.setattr(sys, "argv", ["audit_translations", *args])

audit_cli.main()
output = strip_ansi(capsys.readouterr().out)

assert "Files to check: 1" in output
assert expected_file in output
assert expected_rule in output
assert unexpected_rule not in output


def test_cli_main_accepts_source_language(capsys, monkeypatch) -> None:
"""
Ensure --source changes the reference language without changing target semantics.
Expand Down